From 704c304d57a12f0bf6c73f04d170d7a2bef8c852 Mon Sep 17 00:00:00 2001 From: yy Date: Thu, 23 Apr 2026 14:40:02 +0800 Subject: [PATCH] feat(skills): add skills to create new runtimes --- base-images/languages/go/1.24.13/Dockerfile | 17 ++ base-images/languages/go/1.24.13/build.sh | 52 ++++++ base-images/languages/go/1.25.9/Dockerfile | 17 ++ base-images/languages/go/1.25.9/build.sh | 52 ++++++ .../languages/go/1.24.13/Dockerfile | 21 +++ runtime-images/languages/go/1.24.13/build.sh | 44 +++++ .../1.24.13/project-template/README.en_US.md | 56 ++++++ .../1.24.13/project-template/README.zh_CN.md | 56 ++++++ .../go/1.24.13/project-template/entrypoint.sh | 28 +++ .../go/1.24.13/project-template/main.go | 16 ++ runtime-images/languages/go/1.25.9/Dockerfile | 21 +++ runtime-images/languages/go/1.25.9/build.sh | 44 +++++ .../1.25.9/project-template/README.en_US.md | 56 ++++++ .../1.25.9/project-template/README.zh_CN.md | 56 ++++++ .../go/1.25.9/project-template/entrypoint.sh | 28 +++ .../go/1.25.9/project-template/main.go | 16 ++ skills/runtime-authoring/SKILL.md | 79 ++++++++ skills/runtime-authoring/agents/openai.yaml | 7 + .../references/version-review-checklist.md | 84 +++++++++ .../scripts/scaffold_runtime_version.py | 170 ++++++++++++++++++ .../languages/go/1.24.13/smoke.sh | 77 ++++++++ .../languages/go/1.25.9/smoke.sh | 77 ++++++++ 22 files changed, 1074 insertions(+) create mode 100644 base-images/languages/go/1.24.13/Dockerfile create mode 100644 base-images/languages/go/1.24.13/build.sh create mode 100644 base-images/languages/go/1.25.9/Dockerfile create mode 100644 base-images/languages/go/1.25.9/build.sh create mode 100644 runtime-images/languages/go/1.24.13/Dockerfile create mode 100644 runtime-images/languages/go/1.24.13/build.sh create mode 100644 runtime-images/languages/go/1.24.13/project-template/README.en_US.md create mode 100644 runtime-images/languages/go/1.24.13/project-template/README.zh_CN.md create mode 100755 runtime-images/languages/go/1.24.13/project-template/entrypoint.sh create mode 100644 runtime-images/languages/go/1.24.13/project-template/main.go create mode 100644 runtime-images/languages/go/1.25.9/Dockerfile create mode 100644 runtime-images/languages/go/1.25.9/build.sh create mode 100644 runtime-images/languages/go/1.25.9/project-template/README.en_US.md create mode 100644 runtime-images/languages/go/1.25.9/project-template/README.zh_CN.md create mode 100755 runtime-images/languages/go/1.25.9/project-template/entrypoint.sh create mode 100644 runtime-images/languages/go/1.25.9/project-template/main.go create mode 100644 skills/runtime-authoring/SKILL.md create mode 100644 skills/runtime-authoring/agents/openai.yaml create mode 100644 skills/runtime-authoring/references/version-review-checklist.md create mode 100755 skills/runtime-authoring/scripts/scaffold_runtime_version.py create mode 100755 tests/runtime-smoke/languages/go/1.24.13/smoke.sh create mode 100755 tests/runtime-smoke/languages/go/1.25.9/smoke.sh diff --git a/base-images/languages/go/1.24.13/Dockerfile b/base-images/languages/go/1.24.13/Dockerfile new file mode 100644 index 00000000..345a3734 --- /dev/null +++ b/base-images/languages/go/1.24.13/Dockerfile @@ -0,0 +1,17 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG OS_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/debian-12.6:${OS_IMAGE_VERSION} +LABEL org.opencontainers.image.authors="The Devbox Authors" +# Add build script and execute it +COPY build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh \ No newline at end of file diff --git a/base-images/languages/go/1.24.13/build.sh b/base-images/languages/go/1.24.13/build.sh new file mode 100644 index 00000000..2ff3c78e --- /dev/null +++ b/base-images/languages/go/1.24.13/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} + +# Install Go 1.24.13 +RAW_ARCH="${TARGETARCH:-${ARCH:-$(dpkg --print-architecture)}}" +case "${RAW_ARCH}" in + amd64|x86_64) GO_ARCH=amd64 ;; + arm64|aarch64) GO_ARCH=arm64 ;; + *) + echo "Unsupported architecture: ${RAW_ARCH}" >&2 + exit 1 + ;; +esac +GO_TARBALL="go1.24.13.linux-${GO_ARCH}.tar.gz" +curl -fsSLO "https://dl.google.com/go/${GO_TARBALL}" && \ +rm -rf /usr/local/go && tar -C /usr/local -xzf "${GO_TARBALL}" && \ +rm -f "${GO_TARBALL}" + + +# Set up Go for root +ROOT_HOME="${HOME:-/root}" +if [ "$L10N" = "zh_CN" ]; then + grep -qxF 'export GOPROXY=https://goproxy.cn,direct' "$ROOT_HOME/.bashrc" || \ + echo 'export GOPROXY=https://goproxy.cn,direct' >> "$ROOT_HOME/.bashrc" +fi +mkdir -p "$ROOT_HOME/go/bin" +grep -qxF 'export GOPATH=$HOME/go' "$ROOT_HOME/.bashrc" || \ + echo 'export GOPATH=$HOME/go' >> "$ROOT_HOME/.bashrc" +grep -qxF 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' "$ROOT_HOME/.bashrc" || \ + echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> "$ROOT_HOME/.bashrc" + +# Set up Go for devbox user +DEVBOX_USER="${DEFAULT_DEVBOX_USER}" +DEVBOX_HOME="$(getent passwd "$DEVBOX_USER" | cut -d: -f6 || true)" +if [ -z "$DEVBOX_HOME" ]; then + DEVBOX_HOME="/home/${DEVBOX_USER}" +fi + +if [ "$L10N" = "zh_CN" ]; then + grep -qxF 'export GOPROXY=https://goproxy.cn,direct' "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo 'export GOPROXY=https://goproxy.cn,direct' >> "$DEVBOX_HOME/.bashrc" +fi +mkdir -p "$DEVBOX_HOME/go/bin" +chown -R "${DEVBOX_USER}:${DEVBOX_USER}" "$DEVBOX_HOME/go" || true + +grep -qxF 'export GOPATH=$HOME/go' "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo 'export GOPATH=$HOME/go' >> "$DEVBOX_HOME/.bashrc" +grep -qxF 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> "$DEVBOX_HOME/.bashrc" diff --git a/base-images/languages/go/1.25.9/Dockerfile b/base-images/languages/go/1.25.9/Dockerfile new file mode 100644 index 00000000..345a3734 --- /dev/null +++ b/base-images/languages/go/1.25.9/Dockerfile @@ -0,0 +1,17 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG OS_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/debian-12.6:${OS_IMAGE_VERSION} +LABEL org.opencontainers.image.authors="The Devbox Authors" +# Add build script and execute it +COPY build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh \ No newline at end of file diff --git a/base-images/languages/go/1.25.9/build.sh b/base-images/languages/go/1.25.9/build.sh new file mode 100644 index 00000000..833cd9e9 --- /dev/null +++ b/base-images/languages/go/1.25.9/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} + +# Install Go 1.25.9 +RAW_ARCH="${TARGETARCH:-${ARCH:-$(dpkg --print-architecture)}}" +case "${RAW_ARCH}" in + amd64|x86_64) GO_ARCH=amd64 ;; + arm64|aarch64) GO_ARCH=arm64 ;; + *) + echo "Unsupported architecture: ${RAW_ARCH}" >&2 + exit 1 + ;; +esac +GO_TARBALL="go1.25.9.linux-${GO_ARCH}.tar.gz" +curl -fsSLO "https://dl.google.com/go/${GO_TARBALL}" && \ +rm -rf /usr/local/go && tar -C /usr/local -xzf "${GO_TARBALL}" && \ +rm -f "${GO_TARBALL}" + + +# Set up Go for root +ROOT_HOME="${HOME:-/root}" +if [ "$L10N" = "zh_CN" ]; then + grep -qxF 'export GOPROXY=https://goproxy.cn,direct' "$ROOT_HOME/.bashrc" || \ + echo 'export GOPROXY=https://goproxy.cn,direct' >> "$ROOT_HOME/.bashrc" +fi +mkdir -p "$ROOT_HOME/go/bin" +grep -qxF 'export GOPATH=$HOME/go' "$ROOT_HOME/.bashrc" || \ + echo 'export GOPATH=$HOME/go' >> "$ROOT_HOME/.bashrc" +grep -qxF 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' "$ROOT_HOME/.bashrc" || \ + echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> "$ROOT_HOME/.bashrc" + +# Set up Go for devbox user +DEVBOX_USER="${DEFAULT_DEVBOX_USER}" +DEVBOX_HOME="$(getent passwd "$DEVBOX_USER" | cut -d: -f6 || true)" +if [ -z "$DEVBOX_HOME" ]; then + DEVBOX_HOME="/home/${DEVBOX_USER}" +fi + +if [ "$L10N" = "zh_CN" ]; then + grep -qxF 'export GOPROXY=https://goproxy.cn,direct' "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo 'export GOPROXY=https://goproxy.cn,direct' >> "$DEVBOX_HOME/.bashrc" +fi +mkdir -p "$DEVBOX_HOME/go/bin" +chown -R "${DEVBOX_USER}:${DEVBOX_USER}" "$DEVBOX_HOME/go" || true + +grep -qxF 'export GOPATH=$HOME/go' "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo 'export GOPATH=$HOME/go' >> "$DEVBOX_HOME/.bashrc" +grep -qxF 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> "$DEVBOX_HOME/.bashrc" diff --git a/runtime-images/languages/go/1.24.13/Dockerfile b/runtime-images/languages/go/1.24.13/Dockerfile new file mode 100644 index 00000000..d9a7b9bd --- /dev/null +++ b/runtime-images/languages/go/1.24.13/Dockerfile @@ -0,0 +1,21 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG RUNTIME_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/go-1.24.13:${RUNTIME_IMAGE_VERSION} +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV PROJECT_TEMPLATE_DIR=/project-template +COPY ./project-template ${PROJECT_TEMPLATE_DIR} +COPY ./build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${PROJECT_TEMPLATE_DIR} +# Set the working directory to the default devbox user's project directory +WORKDIR /home/${DEFAULT_DEVBOX_USER}/project diff --git a/runtime-images/languages/go/1.24.13/build.sh b/runtime-images/languages/go/1.24.13/build.sh new file mode 100644 index 00000000..d38f0857 --- /dev/null +++ b/runtime-images/languages/go/1.24.13/build.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +PROJECT_TEMPLATE_DIR=${PROJECT_TEMPLATE_DIR:-/project-template} + +if ! id -u "$DEFAULT_DEVBOX_USER" &>/dev/null; then + echo "User $DEFAULT_DEVBOX_USER does not exist" + exit 1 +fi + +TARGET_DIR="/home/$DEFAULT_DEVBOX_USER/project" +mkdir -p "$TARGET_DIR" + +if [ -f "$PROJECT_TEMPLATE_DIR/README.$L10N.md" ]; then + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md exists. Copying to $TARGET_DIR/README.md" + cp "$PROJECT_TEMPLATE_DIR/README.$L10N.md" "$TARGET_DIR/README.md" +else + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md does not exist. Skipping copy." +fi + +DOCS_DIR=${DOCS_DIR:-/usr/share/devbox/docs} +if [ -f "$DOCS_DIR/README.s6-user-guide.$L10N.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.$L10N.md" "$TARGET_DIR/README.s6-user-guide.md" +elif [ -f "$DOCS_DIR/README.s6-user-guide.en_US.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.en_US.md" "$TARGET_DIR/README.s6-user-guide.md" +fi + +# Copy project template contents (except localized readmes handled above). +# Using `/.` keeps hidden files/dirs if present. +cp -R "${PROJECT_TEMPLATE_DIR}/." "$TARGET_DIR/" + +# If we wrote a localized README.md, remove the localized variants to keep the +# project dir clean (optional; safe if they don't exist). +rm -f "$TARGET_DIR/README.en_US.md" "$TARGET_DIR/README.zh_CN.md" || true + +# Ensure entrypoint is executable if present. +if [ -f "$TARGET_DIR/entrypoint.sh" ]; then + chmod +x "$TARGET_DIR/entrypoint.sh" +fi + +# Set ownership to default devbox user +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$TARGET_DIR" diff --git a/runtime-images/languages/go/1.24.13/project-template/README.en_US.md b/runtime-images/languages/go/1.24.13/project-template/README.en_US.md new file mode 100644 index 00000000..4901c3c8 --- /dev/null +++ b/runtime-images/languages/go/1.24.13/project-template/README.en_US.md @@ -0,0 +1,56 @@ +# Go 1.24.13 Runtime Template + +This template provides a minimal Go HTTP service for DevBox runtime **Go 1.24.13**. + +## Runtime Summary + +- Language version: `Go 1.24.13` +- Base runtime image: `go-1.24.13` +- Entrypoint script: `entrypoint.sh` +- Default service port: `8080` + +## Template Files + +- `main.go`: HTTP server using `net/http` +- `entrypoint.sh`: mode-aware startup script + +## Run in DevBox + +Run commands from `/home/devbox/project`. + +### Development mode + +```bash +bash entrypoint.sh +``` + +Behavior: +- Runs `go run main.go` for fast iteration. + +### Production mode + +```bash +bash entrypoint.sh production +``` + +Behavior: +- Builds binary: `go build -o hello_world main.go` +- Runs binary: `./hello_world` + +## Verify Service + +```bash +curl http://127.0.0.1:8080 +``` + +Expected output: + +```text +Hello, World! +``` + +## Customization + +- Add handlers/routes in `main.go`. +- Replace the single-file layout with a standard module structure when scaling. +- Keep the entrypoint build target aligned with your binary name. diff --git a/runtime-images/languages/go/1.24.13/project-template/README.zh_CN.md b/runtime-images/languages/go/1.24.13/project-template/README.zh_CN.md new file mode 100644 index 00000000..c1e26d8f --- /dev/null +++ b/runtime-images/languages/go/1.24.13/project-template/README.zh_CN.md @@ -0,0 +1,56 @@ +# Go 1.24.13 运行时模板 + +该模板为 DevBox **Go 1.24.13** 运行时提供一个最小可运行的 HTTP 服务。 + +## 运行时概览 + +- 语言版本:`Go 1.24.13` +- 基础运行时镜像:`go-1.24.13` +- 启动脚本:`entrypoint.sh` +- 默认服务端口:`8080` + +## 模板文件 + +- `main.go`:基于 `net/http` 的 HTTP 服务 +- `entrypoint.sh`:支持模式切换的启动脚本 + +## 在 DevBox 中运行 + +以下命令在 `/home/devbox/project` 目录执行。 + +### 开发模式 + +```bash +bash entrypoint.sh +``` + +行为说明: +- 使用 `go run main.go`,便于快速迭代。 + +### 生产模式 + +```bash +bash entrypoint.sh production +``` + +行为说明: +- 先构建二进制:`go build -o hello_world main.go` +- 再运行二进制:`./hello_world` + +## 验证服务 + +```bash +curl http://127.0.0.1:8080 +``` + +预期输出: + +```text +Hello, World! +``` + +## 自定义建议 + +- 在 `main.go` 中增加路由与处理逻辑。 +- 项目变大后可迁移为标准 Go module 目录结构。 +- 若修改二进制名称,请同步更新 `entrypoint.sh` 的构建目标。 diff --git a/runtime-images/languages/go/1.24.13/project-template/entrypoint.sh b/runtime-images/languages/go/1.24.13/project-template/entrypoint.sh new file mode 100755 index 00000000..28946b6e --- /dev/null +++ b/runtime-images/languages/go/1.24.13/project-template/entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/bash +app_env=${1:-development} + +# Define build target +build_target="hello_world" + +# Development environment commands +dev_commands() { + echo "Running development environment commands..." + go run main.go +} + +# Production environment commands +prod_commands() { + echo "Running production environment commands..." + go build -o $build_target main.go + ./$build_target +} + +# prod_commands +# Check environment variables to determine the running environment +if [ "$app_env" = "production" ] || [ "$app_env" = "prod" ] ; then + echo "Production environment detected" + prod_commands +else + echo "Development environment detected" + dev_commands +fi diff --git a/runtime-images/languages/go/1.24.13/project-template/main.go b/runtime-images/languages/go/1.24.13/project-template/main.go new file mode 100644 index 00000000..53952485 --- /dev/null +++ b/runtime-images/languages/go/1.24.13/project-template/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +func main() { + http.HandleFunc("/", handler) + fmt.Println("Starting server on 0.0.0.0:8080") + http.ListenAndServe(":8080", nil) +} diff --git a/runtime-images/languages/go/1.25.9/Dockerfile b/runtime-images/languages/go/1.25.9/Dockerfile new file mode 100644 index 00000000..b1287404 --- /dev/null +++ b/runtime-images/languages/go/1.25.9/Dockerfile @@ -0,0 +1,21 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG RUNTIME_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/go-1.25.9:${RUNTIME_IMAGE_VERSION} +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV PROJECT_TEMPLATE_DIR=/project-template +COPY ./project-template ${PROJECT_TEMPLATE_DIR} +COPY ./build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${PROJECT_TEMPLATE_DIR} +# Set the working directory to the default devbox user's project directory +WORKDIR /home/${DEFAULT_DEVBOX_USER}/project diff --git a/runtime-images/languages/go/1.25.9/build.sh b/runtime-images/languages/go/1.25.9/build.sh new file mode 100644 index 00000000..d38f0857 --- /dev/null +++ b/runtime-images/languages/go/1.25.9/build.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +PROJECT_TEMPLATE_DIR=${PROJECT_TEMPLATE_DIR:-/project-template} + +if ! id -u "$DEFAULT_DEVBOX_USER" &>/dev/null; then + echo "User $DEFAULT_DEVBOX_USER does not exist" + exit 1 +fi + +TARGET_DIR="/home/$DEFAULT_DEVBOX_USER/project" +mkdir -p "$TARGET_DIR" + +if [ -f "$PROJECT_TEMPLATE_DIR/README.$L10N.md" ]; then + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md exists. Copying to $TARGET_DIR/README.md" + cp "$PROJECT_TEMPLATE_DIR/README.$L10N.md" "$TARGET_DIR/README.md" +else + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md does not exist. Skipping copy." +fi + +DOCS_DIR=${DOCS_DIR:-/usr/share/devbox/docs} +if [ -f "$DOCS_DIR/README.s6-user-guide.$L10N.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.$L10N.md" "$TARGET_DIR/README.s6-user-guide.md" +elif [ -f "$DOCS_DIR/README.s6-user-guide.en_US.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.en_US.md" "$TARGET_DIR/README.s6-user-guide.md" +fi + +# Copy project template contents (except localized readmes handled above). +# Using `/.` keeps hidden files/dirs if present. +cp -R "${PROJECT_TEMPLATE_DIR}/." "$TARGET_DIR/" + +# If we wrote a localized README.md, remove the localized variants to keep the +# project dir clean (optional; safe if they don't exist). +rm -f "$TARGET_DIR/README.en_US.md" "$TARGET_DIR/README.zh_CN.md" || true + +# Ensure entrypoint is executable if present. +if [ -f "$TARGET_DIR/entrypoint.sh" ]; then + chmod +x "$TARGET_DIR/entrypoint.sh" +fi + +# Set ownership to default devbox user +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$TARGET_DIR" diff --git a/runtime-images/languages/go/1.25.9/project-template/README.en_US.md b/runtime-images/languages/go/1.25.9/project-template/README.en_US.md new file mode 100644 index 00000000..035d99ec --- /dev/null +++ b/runtime-images/languages/go/1.25.9/project-template/README.en_US.md @@ -0,0 +1,56 @@ +# Go 1.25.9 Runtime Template + +This template provides a minimal Go HTTP service for DevBox runtime **Go 1.25.9**. + +## Runtime Summary + +- Language version: `Go 1.25.9` +- Base runtime image: `go-1.25.9` +- Entrypoint script: `entrypoint.sh` +- Default service port: `8080` + +## Template Files + +- `main.go`: HTTP server using `net/http` +- `entrypoint.sh`: mode-aware startup script + +## Run in DevBox + +Run commands from `/home/devbox/project`. + +### Development mode + +```bash +bash entrypoint.sh +``` + +Behavior: +- Runs `go run main.go` for fast iteration. + +### Production mode + +```bash +bash entrypoint.sh production +``` + +Behavior: +- Builds binary: `go build -o hello_world main.go` +- Runs binary: `./hello_world` + +## Verify Service + +```bash +curl http://127.0.0.1:8080 +``` + +Expected output: + +```text +Hello, World! +``` + +## Customization + +- Add handlers/routes in `main.go`. +- Replace the single-file layout with a standard module structure when scaling. +- Keep the entrypoint build target aligned with your binary name. diff --git a/runtime-images/languages/go/1.25.9/project-template/README.zh_CN.md b/runtime-images/languages/go/1.25.9/project-template/README.zh_CN.md new file mode 100644 index 00000000..9000be6b --- /dev/null +++ b/runtime-images/languages/go/1.25.9/project-template/README.zh_CN.md @@ -0,0 +1,56 @@ +# Go 1.25.9 运行时模板 + +该模板为 DevBox **Go 1.25.9** 运行时提供一个最小可运行的 HTTP 服务。 + +## 运行时概览 + +- 语言版本:`Go 1.25.9` +- 基础运行时镜像:`go-1.25.9` +- 启动脚本:`entrypoint.sh` +- 默认服务端口:`8080` + +## 模板文件 + +- `main.go`:基于 `net/http` 的 HTTP 服务 +- `entrypoint.sh`:支持模式切换的启动脚本 + +## 在 DevBox 中运行 + +以下命令在 `/home/devbox/project` 目录执行。 + +### 开发模式 + +```bash +bash entrypoint.sh +``` + +行为说明: +- 使用 `go run main.go`,便于快速迭代。 + +### 生产模式 + +```bash +bash entrypoint.sh production +``` + +行为说明: +- 先构建二进制:`go build -o hello_world main.go` +- 再运行二进制:`./hello_world` + +## 验证服务 + +```bash +curl http://127.0.0.1:8080 +``` + +预期输出: + +```text +Hello, World! +``` + +## 自定义建议 + +- 在 `main.go` 中增加路由与处理逻辑。 +- 项目变大后可迁移为标准 Go module 目录结构。 +- 若修改二进制名称,请同步更新 `entrypoint.sh` 的构建目标。 diff --git a/runtime-images/languages/go/1.25.9/project-template/entrypoint.sh b/runtime-images/languages/go/1.25.9/project-template/entrypoint.sh new file mode 100755 index 00000000..28946b6e --- /dev/null +++ b/runtime-images/languages/go/1.25.9/project-template/entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/bash +app_env=${1:-development} + +# Define build target +build_target="hello_world" + +# Development environment commands +dev_commands() { + echo "Running development environment commands..." + go run main.go +} + +# Production environment commands +prod_commands() { + echo "Running production environment commands..." + go build -o $build_target main.go + ./$build_target +} + +# prod_commands +# Check environment variables to determine the running environment +if [ "$app_env" = "production" ] || [ "$app_env" = "prod" ] ; then + echo "Production environment detected" + prod_commands +else + echo "Development environment detected" + dev_commands +fi diff --git a/runtime-images/languages/go/1.25.9/project-template/main.go b/runtime-images/languages/go/1.25.9/project-template/main.go new file mode 100644 index 00000000..53952485 --- /dev/null +++ b/runtime-images/languages/go/1.25.9/project-template/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +func main() { + http.HandleFunc("/", handler) + fmt.Println("Starting server on 0.0.0.0:8080") + http.ListenAndServe(":8080", nil) +} diff --git a/skills/runtime-authoring/SKILL.md b/skills/runtime-authoring/SKILL.md new file mode 100644 index 00000000..b15fc2e2 --- /dev/null +++ b/skills/runtime-authoring/SKILL.md @@ -0,0 +1,79 @@ +--- +name: runtime-authoring +description: Use when creating a new DevBox runtime in this repository or adding a new version to an existing runtime family such as Go 1.24 or 1.25. Helps scaffold matching base-images, runtime-images, and tests/runtime-smoke directories, update versioned strings, and validate the result against this repo's CI conventions. +--- + +# Runtime Authoring + +Use this skill for work under `base-images/`, `runtime-images/`, and `tests/runtime-smoke/`. + +## Choose the path + +- Existing runtime family, new version: + use `scripts/scaffold_runtime_version.py` first. This is the fast path for requests like "add Go 1.24" or "add Node.js 24". +- Brand-new runtime family: + do not blindly rename another family. Pick the closest sibling as a reference, then create the new base/runtime/smoke-test directories manually. + +## Repo conventions + +- Runtime definitions usually live in matching directory triplets: + - `base-images////` + - `runtime-images////` + - `tests/runtime-smoke////` when smoke coverage exists +- Valid `` folders are `operating-systems`, `languages`, and `frameworks`. +- CI auto-discovers Dockerfiles. There is usually no central registry list to update after adding a runtime directory. +- Copy the entire version directory tree, not just `Dockerfile` and `build.sh`. Many runtimes also ship `project-template/`, localized `README.*.md`, config files, or extra assets. + +## Existing family to new version + +1. Pick the closest source version from the same family. +2. Run the scaffold script from the repo root: + +```bash +python3 skills/runtime-authoring/scripts/scaffold_runtime_version.py languages go 1.23.0 1.24.0 +``` + +3. Review the new directories carefully. The script only replaces the raw version string; it does not understand semantic changes in package names, download URLs, checksums, entrypoints, or template content. +4. Search the newly created paths for stale source-version strings and family-specific text that still needs manual cleanup. +5. Inspect the nearest existing version diff to see what else changed between releases. +6. Validate the planning logic: + +```bash +python3 .github/scripts/runtime-build.py plan-build \ + --target-kind languages \ + --target-name go/1.24.0 \ + --target-build-type runtime-images \ + --include-prerequisites true +``` + +7. If the runtime has smoke coverage, make sure `tests/runtime-smoke/.../smoke.sh` asserts the new version and still matches the project template layout. + +## Brand-new runtime family + +1. Pick the correct kind: `operating-systems`, `languages`, or `frameworks`. +2. Choose the closest sibling runtime from the same kind as a reference implementation. +3. Create all matching directories together: + - `base-images/...` + - `runtime-images/...` + - `tests/runtime-smoke/...` if the runtime should be smoke-tested +4. Update these files first: + - base `Dockerfile`: parent image and labels + - base `build.sh`: install steps, download URLs, version pins, architecture handling + - runtime `Dockerfile`: `FROM` image name, project-template copy, workdir + - runtime `build.sh`: template placement, ownership, docs copy behavior + - `project-template/*`: localized READMEs, example source, entrypoint + - `smoke.sh`: version assertion, template file checks, startup behavior +5. Prefer following the nearest existing runtime's shape instead of inventing a new layout. +6. Read `../../docs/build-workflows.md` if you need to confirm how CI discovers and publishes the new runtime. + +## Detailed checklist + +Read `references/version-review-checklist.md` when you need the file-level review checklist, search commands, or a Go version-bump example. + +## Output expectations + +When using this skill, finish with: + +- the directories created or changed +- anything still requiring a human decision, such as upstream URL changes, template differences, or missing smoke coverage +- the validation you ran locally versus what still needs CI diff --git a/skills/runtime-authoring/agents/openai.yaml b/skills/runtime-authoring/agents/openai.yaml new file mode 100644 index 00000000..39022c4c --- /dev/null +++ b/skills/runtime-authoring/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Runtime Authoring" + short_description: "Create DevBox runtimes and versions" + default_prompt: "Use $runtime-authoring to add a new runtime or scaffold a new version such as Go 1.24 from the closest existing version." + +policy: + allow_implicit_invocation: true diff --git a/skills/runtime-authoring/references/version-review-checklist.md b/skills/runtime-authoring/references/version-review-checklist.md new file mode 100644 index 00000000..9c5d2b49 --- /dev/null +++ b/skills/runtime-authoring/references/version-review-checklist.md @@ -0,0 +1,84 @@ +# Version Review Checklist + +Use this checklist after scaffolding a new version or when hand-authoring a new runtime family. + +## Always check these paths together + +- `base-images////` +- `runtime-images////` +- `tests/runtime-smoke////` when smoke coverage exists + +## Files that usually need manual review + +- base `Dockerfile` + - parent image + - ARG names and image version variables +- base `build.sh` + - download URL + - pinned version string + - architecture mapping + - checksum or archive name if applicable +- runtime `Dockerfile` + - `FROM` image reference + - copied config or template directories +- runtime `build.sh` + - project-template installation + - permissions and ownership + - docs fallback logic +- `project-template/README.en_US.md` +- `project-template/README.zh_CN.md` +- `project-template/entrypoint.sh` +- extra config files such as `nginx.conf` +- `tests/runtime-smoke/.../smoke.sh` + - version assertion + - expected template files + - process startup behavior + +## Useful searches + +Search for stale source-version strings in the newly created directories: + +```bash +rg -n --hidden '1\.23\.0' \ + base-images/languages/go/1.24.0 \ + runtime-images/languages/go/1.24.0 \ + tests/runtime-smoke/languages/go/1.24.0 +``` + +Search for old image references or family-specific labels: + +```bash +rg -n --hidden 'go-1\.23\.0|Go 1\.23\.0' \ + runtime-images/languages/go/1.24.0 \ + tests/runtime-smoke/languages/go/1.24.0 +``` + +## Planning validation + +Check that CI planning resolves the new runtime and any prerequisite base image: + +```bash +python3 .github/scripts/runtime-build.py plan-build \ + --target-kind languages \ + --target-name go/1.24.0 \ + --target-build-type runtime-images \ + --include-prerequisites true +``` + +If you are touching CI behavior or release expectations, read `docs/build-workflows.md`. + +## Go example + +For a Go version bump, these files usually need version edits: + +- `base-images/languages/go//build.sh` +- `runtime-images/languages/go//Dockerfile` +- `runtime-images/languages/go//project-template/README.en_US.md` +- `runtime-images/languages/go//project-template/README.zh_CN.md` +- `tests/runtime-smoke/languages/go//smoke.sh` + +Typical string changes: + +- `1.23.0` -> `1.24.0` +- `go-1.23.0` -> `go-1.24.0` +- `go1.23.0` -> `go1.24.0` diff --git a/skills/runtime-authoring/scripts/scaffold_runtime_version.py b/skills/runtime-authoring/scripts/scaffold_runtime_version.py new file mode 100755 index 00000000..d83253c2 --- /dev/null +++ b/skills/runtime-authoring/scripts/scaffold_runtime_version.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import re +import shutil +import sys +from pathlib import Path + + +DEFAULT_REPO_ROOT = Path(__file__).resolve().parents[3] +KIND_ALIASES = { + "os": "operating-systems", + "operating-systems": "operating-systems", + "lang": "languages", + "languages": "languages", + "fw": "frameworks", + "frameworks": "frameworks", +} + + +def fail(message: str) -> None: + print(f"Error: {message}", file=sys.stderr) + raise SystemExit(1) + + +def normalize_kind(raw_kind: str) -> str: + kind = KIND_ALIASES.get(raw_kind.strip()) + if not kind: + fail("kind must be one of: operating-systems|languages|frameworks|os|lang|fw") + return kind + + +def version_path(root: Path, prefix: str, kind: str, name: str, version: str) -> Path: + return root / prefix / kind / Path(name) / version + + +def replace_version_in_text_files(target_dir: Path, source_version: str, target_version: str, dry_run: bool) -> list[Path]: + changed_files: list[Path] = [] + for path in sorted(target_dir.rglob("*")): + if not path.is_file(): + continue + try: + original = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + updated = original.replace(source_version, target_version) + if updated == original: + continue + if not dry_run: + path.write_text(updated, encoding="utf-8") + changed_files.append(path) + return changed_files + + +def scaffold_tree(source: Path, target: Path, dry_run: bool) -> None: + if not source.exists(): + fail(f"source path does not exist: {source}") + if target.exists(): + fail(f"target path already exists: {target}") + if dry_run: + return + shutil.copytree(source, target) + + +def relative_to_root(root: Path, path: Path) -> str: + return path.resolve().relative_to(root.resolve()).as_posix() + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Scaffold a new version for an existing runtime family in this repository." + ) + parser.add_argument("kind", help="operating-systems | languages | frameworks (aliases: os | lang | fw)") + parser.add_argument("name", help="Runtime family name, for example go or node.js") + parser.add_argument("source_version", help="Existing version directory to copy from") + parser.add_argument("target_version", help="New version directory to create") + parser.add_argument( + "--repo-root", + type=Path, + default=DEFAULT_REPO_ROOT, + help=f"Repository root (default: {DEFAULT_REPO_ROOT})", + ) + parser.add_argument( + "--skip-smoke-test", + action="store_true", + help="Do not copy tests/runtime-smoke even if a source smoke test exists", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print planned actions without creating files", + ) + args = parser.parse_args() + + repo_root = args.repo_root.resolve() + kind = normalize_kind(args.kind) + name = args.name.strip().strip("/") + source_version = args.source_version.strip() + target_version = args.target_version.strip() + dry_run = bool(args.dry_run) + + if not name: + fail("name cannot be empty") + if not source_version: + fail("source_version cannot be empty") + if not target_version: + fail("target_version cannot be empty") + if source_version == target_version: + fail("source_version and target_version must differ") + + trees = [ + ("base-images", True), + ("runtime-images", True), + ] + if not args.skip_smoke_test: + trees.append(("tests/runtime-smoke", False)) + + created_dirs: list[Path] = [] + changed_files: list[Path] = [] + skipped_optional: list[Path] = [] + + for prefix, required in trees: + source_dir = version_path(repo_root, prefix, kind, name, source_version) + target_dir = version_path(repo_root, prefix, kind, name, target_version) + if not source_dir.exists(): + if required: + fail(f"required source directory does not exist: {source_dir}") + skipped_optional.append(source_dir) + continue + scaffold_tree(source_dir, target_dir, dry_run) + created_dirs.append(target_dir) + changed_files.extend(replace_version_in_text_files(target_dir, source_version, target_version, dry_run)) + + if not created_dirs: + fail("no directories were created") + + print("Scaffolded runtime version:") + for path in created_dirs: + print(f"- {relative_to_root(repo_root, path)}") + + if skipped_optional: + print("Skipped optional source directories:") + for path in skipped_optional: + print(f"- {relative_to_root(repo_root, path)}") + + print(f"Updated text files: {len(changed_files)}") + if changed_files: + for path in changed_files: + print(f"- {relative_to_root(repo_root, path)}") + + escaped_source_version = re.escape(source_version) + print("Next steps:") + print("- Review download URLs, checksums, image references, and project-template content for semantic changes.") + print("- Search for stale source-version strings in the newly created directories:") + print( + " rg -n --hidden " + f"'{escaped_source_version}' " + + " ".join(relative_to_root(repo_root, path) for path in created_dirs) + ) + print("- Run plan-build for the new target before opening a PR.") + if dry_run: + print("Dry run only: no files were written.") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/runtime-smoke/languages/go/1.24.13/smoke.sh b/tests/runtime-smoke/languages/go/1.24.13/smoke.sh new file mode 100755 index 00000000..d36f6d09 --- /dev/null +++ b/tests/runtime-smoke/languages/go/1.24.13/smoke.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -eu + +project_dir=/home/devbox/project + +if [ ! -d "$project_dir" ]; then + echo "Missing project dir: $project_dir" >&2 + exit 1 +fi + +# load profile env (best effort) +set +u +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + [ -r "$f" ] && . "$f" || true + done +fi +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "SHELL=${SHELL:-}" + echo "PATH=$PATH" + for cmd in go python3 node php dotnet java javac gcc g++ cargo rustc; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +cd "$project_dir" + +go version | grep -q 'go1.24.13' + +if [ ! -f "$project_dir/main.go" ]; then + echo "Missing main.go in $project_dir" >&2 + exit 1 +fi + +if [ ! -f "$project_dir/README.md" ]; then + echo "Missing README.md in $project_dir" >&2 + exit 1 +fi + + +# entrypoint smoke +entrypoint="$project_dir/entrypoint.sh" +if [ ! -f "$entrypoint" ]; then + echo "Missing entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +if ! command -v bash >/dev/null 2>&1; then + echo "bash not found" >&2 + exit 1 +fi + +( cd "$project_dir" && bash "$entrypoint" ) >/tmp/entrypoint.log 2>&1 & +pid=$! +sleep 3 +if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "entrypoint exited early" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 +fi +kill "$pid" >/dev/null 2>&1 || true +wait "$pid" >/dev/null 2>&1 || true + +echo "ok" diff --git a/tests/runtime-smoke/languages/go/1.25.9/smoke.sh b/tests/runtime-smoke/languages/go/1.25.9/smoke.sh new file mode 100755 index 00000000..b2c2fee2 --- /dev/null +++ b/tests/runtime-smoke/languages/go/1.25.9/smoke.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -eu + +project_dir=/home/devbox/project + +if [ ! -d "$project_dir" ]; then + echo "Missing project dir: $project_dir" >&2 + exit 1 +fi + +# load profile env (best effort) +set +u +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + [ -r "$f" ] && . "$f" || true + done +fi +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "SHELL=${SHELL:-}" + echo "PATH=$PATH" + for cmd in go python3 node php dotnet java javac gcc g++ cargo rustc; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +cd "$project_dir" + +go version | grep -q 'go1.25.9' + +if [ ! -f "$project_dir/main.go" ]; then + echo "Missing main.go in $project_dir" >&2 + exit 1 +fi + +if [ ! -f "$project_dir/README.md" ]; then + echo "Missing README.md in $project_dir" >&2 + exit 1 +fi + + +# entrypoint smoke +entrypoint="$project_dir/entrypoint.sh" +if [ ! -f "$entrypoint" ]; then + echo "Missing entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +if ! command -v bash >/dev/null 2>&1; then + echo "bash not found" >&2 + exit 1 +fi + +( cd "$project_dir" && bash "$entrypoint" ) >/tmp/entrypoint.log 2>&1 & +pid=$! +sleep 3 +if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "entrypoint exited early" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 +fi +kill "$pid" >/dev/null 2>&1 || true +wait "$pid" >/dev/null 2>&1 || true + +echo "ok"