feat(codex-web-workstation): add Codex CLI web workstation builder

* feat(codex-web-workstation): add image builder with CI workflow

- Ubuntu 24.04 base with dev user, NodeSource 20.x, ttyd 1.7.7, code-server, @openai/codex
- 5 runtime scripts: entrypoint.sh, configure-provider.sh, healthcheck.sh, doctor.sh, smoke-test.sh
- Build parameter resolver and architecture allowlist
- GitHub Actions workflow: build-codex-web-workstation.yml (workflow_dispatch, docker/build-push-action@v6, ghcr.io)
- Image-only: no docker-compose, Caddyfile, or compose-time config

* fix(ci): use hyphenated input names (image-tag, push-latest)

* fix(health): replace curl -sf with tolerant HTTP status check

- healthcheck.sh: accept any 2xx/3xx/4xx response as proof of listening
  (code-server returns 302, ttyd returns 401 with basic auth)
- smoke-test.sh: same fix for HTTP probe tolerance
- Dockerfile: remove duplicate Runtime comment
- .env.example: remove unused USER_UID/USER_GID (not declared as ARG in Dockerfile)
This commit is contained in:
okxlin
2026-06-13 14:11:04 +08:00
committed by GitHub
parent cf41c01051
commit 904e28fa64
12 changed files with 798 additions and 0 deletions
@@ -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"
+107
View File
@@ -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
+18
View File
@@ -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
}
@@ -0,0 +1,6 @@
.env
.env.*
!.env.example
__pycache__/
*.pyc
*.log
@@ -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.).
@@ -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"]
@@ -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}"
+134
View File
@@ -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}=<not set>"
fi
done
check ""
if [ "$status" -eq 0 ]; then
check "=== Doctor: ALL CHECKS PASSED ==="
else
warn "=== Doctor: ${status} CHECK(S) FAILED ==="
fi
exit "$status"
+108
View File
@@ -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" <<EOF
bind-addr: 0.0.0.0:${CODE_SERVER_PORT}
auth: password
password: ${CODE_SERVER_PASSWORD}
cert: false
EOF
# ── 3. Codex 登录提示 ──
CODEX_AUTH_MODE="${CODEX_AUTH_MODE:-device-auth}"
case "${CODEX_AUTH_MODE}" in
device-auth)
echo "============================================================"
echo " Codex login required. Run the following command:"
echo " codex login --device-auth"
echo ""
echo " Open the URL shown in the terminal in any browser,"
echo " enter the verification code to complete authentication."
echo " Login state persists in the codex-home volume."
echo "============================================================"
;;
api-key)
if [ -z "${OPENAI_API_KEY:-}" ]; then
echo "WARNING: CODEX_AUTH_MODE=api-key but OPENAI_API_KEY is not set." >&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
+29
View File
@@ -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
+85
View File
@@ -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"
@@ -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}" <<EOF
image-repo=${IMAGE_REPO}
platforms=${PLATFORMS}
image-tag=${IMAGE_TAG}
push-latest=${PUSH_LATEST}
latest-tag=${LATEST_TAG}
EOF
fi
echo "image-repo=${IMAGE_REPO}"
echo "platforms=${PLATFORMS}"
echo "image-tag=${IMAGE_TAG}"
echo "push-latest=${PUSH_LATEST}"
echo "latest-tag=${LATEST_TAG}"