diff --git a/.github/workflows/build-codex-web-workstation.yml b/.github/workflows/build-codex-web-workstation.yml new file mode 100644 index 0000000..8ac237c --- /dev/null +++ b/.github/workflows/build-codex-web-workstation.yml @@ -0,0 +1,104 @@ +name: Build Codex Web Workstation Image + +on: + workflow_dispatch: + inputs: + image-tag: + description: "Published image tag; leave empty to use latest" + required: false + default: "latest" + platforms: + description: "Comma-separated target platforms" + required: true + default: "linux/amd64" + push-latest: + description: "Also publish latest tag" + required: true + default: false + type: boolean + +concurrency: + group: build-codex-web-workstation-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE_REPO: codex-web-workstation + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Normalize repository owner + id: owner + run: | + LOWER_OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + echo "lower_owner=$LOWER_OWNER" >> "$GITHUB_OUTPUT" + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Resolve build parameters + id: params + run: | + chmod +x codex-web-workstation-builder/scripts/*.sh + bash codex-web-workstation-builder/scripts/resolve-build-params.sh \ + --image-repo "${IMAGE_REPO}" \ + --platforms "${{ github.event.inputs.platforms || 'linux/amd64' }}" \ + --image-tag "${{ github.event.inputs['image-tag'] || 'latest' }}" \ + --push-latest "${{ github.event.inputs['push-latest'] == 'true' && 'true' || 'false' }}" \ + --latest-tag "latest" \ + --github-output "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ steps.owner.outputs.lower_owner }}/${{ steps.params.outputs.image_repo }} + tags: ${{ steps.params.outputs.tags }} + labels: | + org.opencontainers.image.title=Codex Web Workstation + org.opencontainers.image.description=Browser-accessible workstation with Codex CLI, code-server, and ttyd in a single Docker image. + org.opencontainers.image.vendor=${{ github.repository_owner }} + org.opencontainers.image.source=https://github.com/${{ github.repository }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: ./codex-web-workstation-builder/image + file: ./codex-web-workstation-builder/image/Dockerfile + platforms: ${{ steps.params.outputs.platforms }} + push: true + build-args: | + TTYD_VERSION=1.7.7 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Emit build summary + run: | + { + echo "## Codex Web Workstation image build" + echo "" + echo "- Image: ghcr.io/${{ steps.owner.outputs.lower_owner }}/${{ steps.params.outputs.image_repo }}" + echo "- Published image tag: ${{ steps.params.outputs.image_tag }}" + echo "- Tags: ${{ steps.meta.outputs.tags }}" + echo "- Platforms: ${{ steps.params.outputs.platforms }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/codex-web-workstation-builder/README.md b/codex-web-workstation-builder/README.md new file mode 100644 index 0000000..bace000 --- /dev/null +++ b/codex-web-workstation-builder/README.md @@ -0,0 +1,107 @@ +# codex-web-workstation-builder + +Docker image builder for the Codex Web Workstation — a browser-accessible Linux development environment with Codex CLI, code-server (VS Code), and ttyd (web terminal). + +## Directory Conventions + +``` +codex-web-workstation-builder/ +├── README.md # This file +├── configs/ +│ └── architectures.sh # Supported build platforms +├── scripts/ +│ └── resolve-build-params.sh # CI build parameter resolver +└── image/ + ├── .dockerignore + ├── .env.example # Runtime env vars reference + ├── Dockerfile # Ubuntu 24.04 + Node 20 + toolchain + ├── docker-compose.yml # workstation + caddy services + ├── Caddyfile # HTTPS + Basic Auth + reverse proxy + ├── scripts/ + │ ├── entrypoint.sh # Container entrypoint (9 steps) + │ ├── configure-provider.sh # Custom provider config generator + │ ├── healthcheck.sh # Docker HEALTHCHECK script + │ ├── doctor.sh # Full diagnostic + │ └── smoke-test.sh # Quick smoke test + └── config/ + ├── codex/ # Codex CLI config examples + └── code-server/ # code-server config example +``` + +Build-time code (`configs/`, `scripts/`) stays outside the image. Runtime code (`image/scripts/`, `image/config/`) gets baked into the container. + +## Build + +```bash +cd image +docker compose build +``` + +The Docker context is `image/`. The Dockerfile expects all COPY paths relative to this directory. + +## Runtime Environment + +Copy `image/.env.example` to `image/.env` and fill in real values: + +```bash +cp image/.env.example image/.env +# Edit .env — at minimum set DOMAIN, CODE_SERVER_PASSWORD, and BASIC_AUTH_HASH +``` + +Generate Caddy password hash: + +```bash +docker run --rm caddy:2 caddy hash-password --plaintext 'your-password' +``` + +Start: + +```bash +cd image +docker compose up -d +``` + +Services are accessible at: +- `https://DOMAIN/ide/` — code-server (VS Code) +- `https://DOMAIN/terminal/` — ttyd web terminal + +## Diagnostics + +```bash +# Full diagnostic report +docker compose exec workstation doctor.sh + +# Quick smoke test +docker compose exec workstation smoke-test.sh +``` + +## CI Integration + +```bash +source scripts/resolve-build-params.sh \ + --image-repo ghcr.io/org/codex-web-workstation \ + --platforms linux/amd64 \ + --image-tag v1.0.0 \ + --github-output "$GITHUB_OUTPUT" +``` + +Outputs `image-repo`, `platforms`, `image-tag` for downstream workflow steps. + +## What PR Reviewers Should Check + +- **Supported platforms** in `configs/architectures.sh` match the PR scope +- **Dockerfile** installs no experimental or unreleased packages +- **entrypoint.sh** does not auto-login, does not print secrets +- **Caddyfile** correctly handles `/ide/*` and `/terminal/*` path stripping +- **healthcheck.sh** covers both code-server (8080) and ttyd (7681) +- **.env.example** documents all runtime environment variables + +## Key Design Decisions + +- **No Codex App Server** — not started by default; WebSocket transport is experimental +- **No CodexPlusPlus** — targets Codex Desktop App, CDP injection doesn't work in headless Docker +- **No Happy CLI by default** — `ENABLE_HAPPY_REMOTE=false`, docs-only +- **Three-layer auth** — Caddy Basic Auth → code-server password → ttyd credentials +- **Custom provider** — non-interactive `configure-provider.sh`, requires Responses API support +- **Chat Completions-only APIs** — not supported; provider must implement OpenAI Responses API +- **Multi-arch** — MVP `linux/amd64` only; `arm64` planned for later release \ No newline at end of file diff --git a/codex-web-workstation-builder/configs/architectures.sh b/codex-web-workstation-builder/configs/architectures.sh new file mode 100755 index 0000000..aa1ffae --- /dev/null +++ b/codex-web-workstation-builder/configs/architectures.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# architectures.sh — supported build platforms for codex-web-workstation +# Sourced by scripts/resolve-build-params.sh + +SUPPORTED_PLATFORMS=( + "linux/amd64" + # "linux/arm64" # Future: enable after multi-arch support +) + +is_supported_platform() { + local platform="$1" + for supported in "${SUPPORTED_PLATFORMS[@]}"; do + if [ "$platform" = "$supported" ]; then + return 0 + fi + done + return 1 +} \ No newline at end of file diff --git a/codex-web-workstation-builder/image/.dockerignore b/codex-web-workstation-builder/image/.dockerignore new file mode 100644 index 0000000..a5bf363 --- /dev/null +++ b/codex-web-workstation-builder/image/.dockerignore @@ -0,0 +1,6 @@ +.env +.env.* +!.env.example +__pycache__/ +*.pyc +*.log \ No newline at end of file diff --git a/codex-web-workstation-builder/image/.env.example b/codex-web-workstation-builder/image/.env.example new file mode 100644 index 0000000..8c7d9ab --- /dev/null +++ b/codex-web-workstation-builder/image/.env.example @@ -0,0 +1,13 @@ +# codex-web-workstation build-time environment +# These are passed as --build-arg or through the CI workflow config. +# Runtime secrets are never baked into the image; inject them at deploy time. + +# User credentials inside the image (default: dev user, UID 1000) + +# ttyd version (from GitHub releases) +TTYD_VERSION=1.7.7 + +# Runtime secrets: inject at deploy time, never commit real values. +# Keep API keys, tokens, and provider credentials out of this repository. +# Recommended: export PASSWORD=***, export CODEX_AUTH_MODE=***, etc. +# Then pass them through your deploy system (docker run -e, compose env_file, etc.). \ No newline at end of file diff --git a/codex-web-workstation-builder/image/Dockerfile b/codex-web-workstation-builder/image/Dockerfile new file mode 100644 index 0000000..5e2b5ab --- /dev/null +++ b/codex-web-workstation-builder/image/Dockerfile @@ -0,0 +1,81 @@ +# codex-web-workstation Dockerfile +# Ubuntu 24.04 base, amd64 only for MVP +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive + +# ── System packages ── +RUN apt-get update && apt-get install -y --no-install-recommends \ + bat \ + bash \ + build-essential \ + ca-certificates \ + curl \ + fd-find \ + fzf \ + git \ + git-lfs \ + htop \ + httpie \ + iproute2 \ + iputils-ping \ + jq \ + ncdu \ + net-tools \ + openssh-client \ + pkg-config \ + python3 \ + python3-pip \ + python3-venv \ + ripgrep \ + sudo \ + tig \ + tini \ + tmux \ + tree \ + zstd \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/bin/fdfind /usr/local/bin/fd \ + && ln -sf /usr/bin/batcat /usr/local/bin/bat + +# ── Node.js 20.x via NodeSource ── +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g npm@latest + +# ── ttyd from GitHub releases ── +ARG TTYD_VERSION=1.7.7 +RUN arch=$(dpkg --print-architecture) && \ + ttyd_arch=$(echo "$arch" | sed 's/amd64/x86_64/' | sed 's/arm64/aarch64/') && \ + curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}" \ + -o /usr/local/bin/ttyd \ + && chmod +x /usr/local/bin/ttyd + +# ── code-server ── +RUN curl -fsSL https://code-server.dev/install.sh | sh + +# ── Codex CLI ── +RUN npm install -g @openai/codex + +# ── User and directories ── +RUN useradd -m -s /bin/bash dev \ + && mkdir -p /workspace /run/codex \ + && chown -R dev:dev /workspace /run/codex /home/dev + +# ── Scripts ── +COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY scripts/healthcheck.sh /usr/local/bin/healthcheck.sh +COPY scripts/configure-provider.sh /usr/local/bin/configure-provider.sh +COPY scripts/doctor.sh /usr/local/bin/doctor.sh +COPY scripts/smoke-test.sh /usr/local/bin/smoke-test.sh +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh /usr/local/bin/configure-provider.sh /usr/local/bin/doctor.sh /usr/local/bin/smoke-test.sh + +# ── Runtime ── +USER dev +WORKDIR /workspace + +EXPOSE 8080 7681 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["/usr/local/bin/entrypoint.sh"] \ No newline at end of file diff --git a/codex-web-workstation-builder/image/scripts/configure-provider.sh b/codex-web-workstation-builder/image/scripts/configure-provider.sh new file mode 100755 index 0000000..ce752d8 --- /dev/null +++ b/codex-web-workstation-builder/image/scripts/configure-provider.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# configure-provider.sh — 非交互式生成 Codex 自定义 provider 配置 +# 由 entrypoint.sh 在 ENABLE_CUSTOM_PROVIDER=true 时调用 +# 读取环境变量,写入 ~/.codex/config.toml provider 段落 + +set -euo pipefail + +CODEX_HOME="${CODEX_HOME:-/home/dev}" +CODEX_CONFIG="${CODEX_HOME}/.codex/config.toml" +PROVIDER_DIR="${CODEX_HOME}/.codex" + +ENABLE_CUSTOM_PROVIDER="${ENABLE_CUSTOM_PROVIDER:-false}" +CUSTOM_PROVIDER_NAME="${CUSTOM_PROVIDER_NAME:-custom}" +CUSTOM_PROVIDER_BASE_URL="${CUSTOM_PROVIDER_BASE_URL:-}" +CUSTOM_PROVIDER_MODEL="${CUSTOM_PROVIDER_MODEL:-}" +CUSTOM_PROVIDER_ENV_KEY="${CUSTOM_PROVIDER_ENV_KEY:-CUSTOM_API_KEY}" + +if [ "${ENABLE_CUSTOM_PROVIDER}" != "true" ]; then + exit 0 +fi + +if [ -z "${CUSTOM_PROVIDER_BASE_URL}" ] || [ -z "${CUSTOM_PROVIDER_MODEL}" ]; then + echo "ERROR: ENABLE_CUSTOM_PROVIDER=true but CUSTOM_PROVIDER_BASE_URL or CUSTOM_PROVIDER_MODEL is empty." >&2 + echo " Set both variables or set ENABLE_CUSTOM_PROVIDER=false." >&2 + exit 1 +fi + +mkdir -p "${PROVIDER_DIR}" + +# Ensure config.toml exists +touch "${CODEX_CONFIG}" + +# Check if provider section already exists +if grep -q "\\[model_providers\\.${CUSTOM_PROVIDER_NAME}\\]" "${CODEX_CONFIG}" 2>/dev/null; then + echo "Provider [model_providers.${CUSTOM_PROVIDER_NAME}] already exists in ${CODEX_CONFIG}, skipping." + exit 0 +fi + +# Append provider section +# Use env_key — never write API key plaintext into config +{ + echo "" + echo "[model_providers.${CUSTOM_PROVIDER_NAME}]" + echo "name = \"${CUSTOM_PROVIDER_NAME}\"" + echo "base_url = \"${CUSTOM_PROVIDER_BASE_URL}\"" + echo "env_key = \"${CUSTOM_PROVIDER_ENV_KEY}\"" + echo "wire_api = \"responses\"" +} >> "${CODEX_CONFIG}" + +echo "Provider [model_providers.${CUSTOM_PROVIDER_NAME}] written to ${CODEX_CONFIG}" \ No newline at end of file diff --git a/codex-web-workstation-builder/image/scripts/doctor.sh b/codex-web-workstation-builder/image/scripts/doctor.sh new file mode 100755 index 0000000..db67527 --- /dev/null +++ b/codex-web-workstation-builder/image/scripts/doctor.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# doctor.sh — 诊断 codex-web-workstation 运行环境 +set -euo pipefail + +status=0 + +check() { printf '[doctor] %s\n' "$*"; } +warn() { printf '[doctor] WARN: %s\n' "$*" >&2; } + +check "=== System Environment ===" +check "hostname: $(hostname 2>/dev/null || echo unknown)" +check "user: $(whoami 2>/dev/null || id -un)" +check "home: ${HOME:-not set}" +check "workspace: ${CONTAINER_WORKSPACE:-/workspace}" + +check "" +check "=== Runtime Versions ===" + +check "node version" +if command -v node >/dev/null 2>&1; then + node --version +else + warn "node not found" + status=1 +fi + +check "npm version" +if command -v npm >/dev/null 2>&1; then + npm --version +else + warn "npm not found" + status=1 +fi + +check "python3 version" +python3 --version 2>/dev/null || warn "python3 not found" + +check "" +check "=== Core Services ===" + +check "code-server" +if command -v code-server >/dev/null 2>&1; then + code-server --version 2>/dev/null | head -1 || true +else + warn "code-server not installed" + status=1 +fi + +check "ttyd" +if command -v ttyd >/dev/null 2>&1; then + ttyd --version 2>/dev/null || true +else + warn "ttyd not installed" + status=1 +fi + +check "codex" +if command -v codex >/dev/null 2>&1; then + codex --version 2>/dev/null || true +else + warn "codex not installed" + status=1 +fi + +check "" +check "=== Development Toolchain ===" +for cmd in git curl jq rg fd make gcc g++; do + if command -v "$cmd" >/dev/null 2>&1; then + check "$cmd: available" + else + warn "$cmd: missing" + status=1 + fi +done + +check "" +check "=== Directories ===" +for dir in /workspace /home/dev /home/dev/.codex /home/dev/.config/code-server /run/codex; do + if [ -d "$dir" ]; then + if [ -w "$dir" ]; then + check "$dir: exists and writable" + else + warn "$dir: exists but NOT writable" + status=1 + fi + else + warn "$dir: missing" + status=1 + fi +done + +check "" +check "=== Config Files ===" +CONFIG_FILES=( + "/home/dev/.config/code-server/config.yaml" + "/home/dev/.codex/config.toml" +) +for f in "${CONFIG_FILES[@]}"; do + if [ -f "$f" ]; then + check "$f: present" + else + warn "$f: missing (may be generated at runtime)" + fi +done + +check "" +check "=== Service Ports ===" +for port in 8080 7681; do + if ss -tlnp 2>/dev/null | grep -q ":${port} " || true; then + check "port ${port}: listening" + else + warn "port ${port}: not listening (services may not be started yet)" + fi +done + +check "" +check "=== Environment Variables ===" +for var in PASSWORD CODEX_AUTH_MODE ENABLE_CUSTOM_PROVIDER ENABLE_HAPPY_REMOTE; do + val="${!var:-}" + if [ -n "$val" ]; then + check "${var}=${val}" + else + check "${var}=" + fi +done + +check "" +if [ "$status" -eq 0 ]; then + check "=== Doctor: ALL CHECKS PASSED ===" +else + warn "=== Doctor: ${status} CHECK(S) FAILED ===" +fi + +exit "$status" diff --git a/codex-web-workstation-builder/image/scripts/entrypoint.sh b/codex-web-workstation-builder/image/scripts/entrypoint.sh new file mode 100755 index 0000000..9f0b9be --- /dev/null +++ b/codex-web-workstation-builder/image/scripts/entrypoint.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# entrypoint.sh — codex-web-workstation 容器入口 +set -euo pipefail + +CODEX_HOME="/home/dev" +CODE_SERVER_PORT=8080 +TTYD_PORT=7681 + +# ── 1. 创建必要目录(不覆盖已有内容) ── +mkdir -p /workspace +mkdir -p "${CODEX_HOME}/.codex" +mkdir -p "${CODEX_HOME}/.config/code-server" +mkdir -p /run/codex + +# ── 2. 写入 code-server 配置 ── +CODE_SERVER_PASSWORD="${PASSWORD:-change-me}" +cat > "${CODEX_HOME}/.config/code-server/config.yaml" <&2 + echo " Set OPENAI_API_KEY in .env or environment." >&2 + else + echo "OPENAI_API_KEY is set. Use: printenv OPENAI_API_KEY | codex login --with-api-key" + fi + ;; + access-token) + if [ -z "${CODEX_ACCESS_TOKEN:-}" ]; then + echo "WARNING: CODEX_AUTH_MODE=access-token but CODEX_ACCESS_TOKEN is not set." >&2 + fi + # Do not print the token value + echo "CODEX_ACCESS_TOKEN is set. Use: codex login --with-access-token" + ;; + none) + echo "Codex login skipped (CODEX_AUTH_MODE=none). Assuming persisted auth state." + ;; + *) + echo "WARNING: Unknown CODEX_AUTH_MODE='${CODEX_AUTH_MODE}'. Expected: device-auth|api-key|access-token|none" >&2 + ;; +esac + +# ── 4. 自定义 Provider 配置 ── +export ENABLE_CUSTOM_PROVIDER +export CUSTOM_PROVIDER_NAME CUSTOM_PROVIDER_BASE_URL CUSTOM_PROVIDER_MODEL CUSTOM_PROVIDER_ENV_KEY +if [ "${ENABLE_CUSTOM_PROVIDER:-false}" = "true" ]; then + /usr/local/bin/configure-provider.sh +fi + +# ── 5. 确保 /home/dev 目录权限 ── +# If running as root initially, fix ownership then switch +if [ "$(id -u)" = "0" ]; then + chown -R dev:dev "${CODEX_HOME}" /workspace /run/codex 2>/dev/null || true +fi + +# ── 6. 启动 code-server ── +code-server /workspace > /tmp/code-server.log 2>&1 & +CODE_SERVER_PID=$! +echo "code-server started (PID ${CODE_SERVER_PID}) on port ${CODE_SERVER_PORT}" + +# ── 7. 启动 ttyd ── +TTYD_USER="${TTYD_USER:-dev}" +TTYD_PASSWORD="${TTYD_PASSWORD:-change-me}" +ttyd -p ${TTYD_PORT} -W --base-path /terminal \ + -c "${TTYD_USER}:${TTYD_PASSWORD}" \ + bash -lc 'cd /workspace && exec bash' > /tmp/ttyd.log 2>&1 & +TTYD_PID=$! +echo "ttyd started (PID ${TTYD_PID}) on port ${TTYD_PORT}" + +# ── 8. Happy 远程控制提示 ── +ENABLE_HAPPY_REMOTE="${ENABLE_HAPPY_REMOTE:-false}" +HAPPY_SERVER_URL="${HAPPY_SERVER_URL:-}" +if [ "${ENABLE_HAPPY_REMOTE}" = "true" ] && [ -n "${HAPPY_SERVER_URL}" ]; then + echo "============================================================" + echo " Happy remote control is enabled." + echo " HAPPY_SERVER_URL=${HAPPY_SERVER_URL}" + echo "" + echo " To start a remote-controllable Codex session, run:" + echo " happy codex" + echo "" + echo " Note: happy CLI must be installed separately." + echo " npm install -g happy" + echo "============================================================" +fi + +# ── 9. Wait for any process to exit ── +echo "All services started. Waiting for processes..." +wait -n +echo "A service process exited. Shutting down..." +kill ${CODE_SERVER_PID} ${TTYD_PID} 2>/dev/null || true +wait \ No newline at end of file diff --git a/codex-web-workstation-builder/image/scripts/healthcheck.sh b/codex-web-workstation-builder/image/scripts/healthcheck.sh new file mode 100755 index 0000000..5d75cb5 --- /dev/null +++ b/codex-web-workstation-builder/image/scripts/healthcheck.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# healthcheck.sh — 容器健康检查 +# 检查 code-server 和 ttyd 是否在监听 + +set -euo pipefail + +CODE_SERVER_PORT=8080 +TTYD_PORT=7681 + +# Check code-server is listening +if ! curl -s -o /dev/null -w "%{http_code}" "http://localhost:${CODE_SERVER_PORT}/" | grep -qE '^[2345][0-9]{2}$'; then + echo "code-server not responding on port ${CODE_SERVER_PORT}" >&2 + exit 1 +fi + +# Check ttyd is listening +if ! curl -s -o /dev/null -w "%{http_code}" "http://localhost:${TTYD_PORT}/" | grep -qE '^[2345][0-9]{2}$'; then + echo "ttyd not responding on port ${TTYD_PORT}" >&2 + exit 1 +fi + +# Check workspace directory is accessible +if [ ! -d /workspace ]; then + echo "/workspace directory not found" >&2 + exit 1 +fi + +echo "healthy" +exit 0 \ No newline at end of file diff --git a/codex-web-workstation-builder/image/scripts/smoke-test.sh b/codex-web-workstation-builder/image/scripts/smoke-test.sh new file mode 100755 index 0000000..d12f61b --- /dev/null +++ b/codex-web-workstation-builder/image/scripts/smoke-test.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# smoke-test.sh — 冒烟测试:快速验证核心服务可用性 +set -euo pipefail + +status=0 + +pass() { printf '[smoke] PASS: %s\n' "$*"; } +fail() { printf '[smoke] FAIL: %s\n' "$*" >&2; status=1; } + +echo "[smoke] codex-web-workstation smoke test" +echo "[smoke] $(date -u +%Y-%m-%dT%H:%M:%SZ)" + +echo "" +echo "[smoke] === Required Commands ===" +REQUIRED_CMDS=(node npm code-server ttyd codex git curl jq rg fd python3 make gcc g++) +for cmd in "${REQUIRED_CMDS[@]}"; do + if command -v "$cmd" >/dev/null 2>&1; then + pass "$cmd available" + else + fail "$cmd missing" + fi +done + +echo "" +echo "[smoke] === Command Versions ===" +command -v node >/dev/null && node --version && pass "node" || fail "node" +command -v codex >/dev/null && codex --version && pass "codex" || fail "codex" + +echo "" +echo "[smoke] === Core Services (HTTP probes) ===" + +CODE_SERVER_PORT="${CODE_SERVER_PORT:-8080}" +TTYD_PORT="${TTYD_PORT:-7681}" + +# Accept any HTTP response (2xx/3xx/4xx) as proof of listening +if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${CODE_SERVER_PORT}/" | grep -qE '^[2345][0-9]{2}$'; then + pass "code-server responding on port ${CODE_SERVER_PORT}" +else + fail "code-server not responding on port ${CODE_SERVER_PORT}" +fi + +# Accept any HTTP response (2xx/3xx/4xx) as proof of listening +if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${TTYD_PORT}/" | grep -qE '^[2345][0-9]{2}$'; then + pass "ttyd responding on port ${TTYD_PORT}" +else + fail "ttyd not responding on port ${TTYD_PORT}" +fi + +echo "" +echo "[smoke] === Workspace Directory ===" +WORKSPACE="${CONTAINER_WORKSPACE:-/workspace}" +if [ -d "$WORKSPACE" ] && [ -w "$WORKSPACE" ]; then + pass "${WORKSPACE} exists and writable" +else + fail "${WORKSPACE} missing or not writable" +fi + +echo "" +echo "[smoke] === Persistence Check ===" +for dir in /home/dev/.codex /home/dev/.config/code-server; do + if [ -d "$dir" ]; then + pass "${dir} present" + else + fail "${dir} missing" + fi +done + +echo "" +echo "[smoke] === Scripts Installed ===" +for script in entrypoint.sh healthcheck.sh configure-provider.sh doctor.sh smoke-test.sh; do + if [ -x "/usr/local/bin/${script}" ]; then + pass "${script} installed and executable" + else + fail "${script} missing or not executable" + fi +done + +echo "" +if [ "$status" -eq 0 ]; then + echo "[smoke] ALL TESTS PASSED" +else + echo "[smoke] ${status} TEST(S) FAILED" >&2 +fi + +exit "$status" diff --git a/codex-web-workstation-builder/scripts/resolve-build-params.sh b/codex-web-workstation-builder/scripts/resolve-build-params.sh new file mode 100755 index 0000000..b7ac3b4 --- /dev/null +++ b/codex-web-workstation-builder/scripts/resolve-build-params.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# resolve-build-params.sh — CI-facing build parameter resolver +# Parses workflow inputs, validates platforms, sanitizes tags, +# and writes outputs for downstream workflow steps. +# +# Usage: source scripts/resolve-build-params.sh [--image-repo REPO] [--platforms LIST] [--image-tag TAG] [--push-latest BOOL] [--latest-tag TAG] [--github-output FILE] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../configs/architectures.sh" + +# Defaults +IMAGE_REPO="codex-web-workstation" +PLATFORMS="linux/amd64" +IMAGE_TAG="latest" +PUSH_LATEST="false" +LATEST_TAG="latest" +GITHUB_OUTPUT="${GITHUB_OUTPUT:-/dev/null}" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --image-repo) IMAGE_REPO="$2"; shift 2 ;; + --platforms) PLATFORMS="$2"; shift 2 ;; + --image-tag) IMAGE_TAG="$2"; shift 2 ;; + --push-latest) PUSH_LATEST="$2"; shift 2 ;; + --latest-tag) LATEST_TAG="$2"; shift 2 ;; + --github-output) GITHUB_OUTPUT="$2"; shift 2 ;; + *) shift ;; + esac +done + +# Sanitize tag: strip refs/tags/ and leading 'v' +IMAGE_TAG="${IMAGE_TAG#refs/tags/}" +IMAGE_TAG="${IMAGE_TAG#v}" + +# Validate platforms +IFS=',' read -ra PLATFORM_LIST <<< "$PLATFORMS" +for platform in "${PLATFORM_LIST[@]}"; do + platform="$(echo "$platform" | xargs)" + if ! is_supported_platform "$platform"; then + echo "ERROR: Unsupported platform: $platform" >&2 + echo "Supported: ${SUPPORTED_PLATFORMS[*]}" >&2 + exit 1 + fi +done + +# Write GitHub Actions outputs +if [ -n "${GITHUB_OUTPUT}" ] && [ "${GITHUB_OUTPUT}" != "/dev/null" ]; then + cat >> "${GITHUB_OUTPUT}" <