mirror of
https://github.com/okxlin/release-factory.git
synced 2026-09-22 00:02:16 +00:00
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:
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user