mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
* feat(docker): parameterize Dockerfile.integrated with ARG FLAVOR=minimal|full Add FLAVOR build arg that gates the AI CLI install layer (claude-code, gemini-cli, grok-cli, opencode) so one Dockerfile produces both the minimal (< 350 MB) and full (< 600 MB) integrated images. Use BuildKit cache mount for /root/.npm to speed up repeated builds. Part of #1251 (P1 — publish integrated images). * feat(docker): emit startup deprecation warning in legacy ccs-dashboard entrypoint Prepend a [WARN] line to stderr on every container start so operators running ghcr.io/kaitranntt/ccs-dashboard:latest are notified to migrate to ghcr.io/kaitranntt/ccs:latest. Sunset window: 2 releases. See #1251. * test(docker): add image-size.sh budget assertion + unit test suite image-size.sh: asserts docker image inspect .Size against a byte budget. Usage: image-size.sh <image:tag> <max-bytes> Budgets: minimal=350 MB (367001600), full=600 MB (629145600) image-size-logic.test.sh: 6 mock-docker unit tests covering pass/fail boundaries, bad arg count, and non-integer input. All 6 pass locally. Part of #1251 (P1). * ci(docker): extend docker-release.yml to publish ccs:latest and ccs:full - Add publish-integrated job with matrix flavor: [minimal, full] - minimal -> ghcr.io/kaitranntt/ccs:<ver> + :latest (when promoted) - full -> ghcr.io/kaitranntt/ccs:full-<ver> + :full (when promoted) - Multi-arch: linux/amd64 + linux/arm64 via buildx - Scoped GHA cache per flavor to avoid cross-contamination - Add promote_to_latest workflow_dispatch input (default false) - rc.1 soak: first publish only pushes immutable version tag - Mutable :latest/:full promoted only on release event OR explicit opt-in - Add smoke-test job (post-publish) for each flavor - Pulls the just-published version tag - Asserts image size via tests/docker/image-size.sh - Boots container, waits for healthcheck, probes :3000 and :8317 - Keep publish-dashboard job unchanged (legacy 2-release sunset) - Updated labels to note deprecation status All jobs run on self-hosted cliproxy runners. Part of #1251 (P1). * docs(docker): document new image tags, add ccs-dashboard deprecation notices docker/README.md: - Add "Choosing an image" table (ccs:latest / ccs:full / ccs-dashboard deprecated) - Update Quick Start section to use ccs:latest as primary example - Add legacy image note with sunset timeline CHANGELOG.md: - Add Unreleased > ### Deprecated entry for ccs-dashboard:latest pointing to migration path and #1251 README.md: - Add one-line deprecation banner near top routing users to ccs:latest Part of #1251 (P1).
This commit is contained in:
@@ -10,15 +10,27 @@ on:
|
||||
description: Stable tag to publish manually, for example v7.55.0
|
||||
required: true
|
||||
type: string
|
||||
promote_to_latest:
|
||||
description: >
|
||||
Promote mutable :latest / :full tags (and major/minor aliases).
|
||||
Use only after a successful rc.1 soak period. Defaults to false so
|
||||
the first publish of a tag only pushes the immutable version tags.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: docker-release-${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 1: Legacy ccs-dashboard image (2-release sunset window)
|
||||
# ---------------------------------------------------------------------------
|
||||
publish-dashboard:
|
||||
name: Publish legacy ccs-dashboard image
|
||||
if: ${{ github.event_name != 'release' || !github.event.release.prerelease }}
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
runs-on: [self-hosted, linux, x64, cliproxy]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -36,7 +48,6 @@ jobs:
|
||||
else
|
||||
TARGET_TAG="${MANUAL_TAG}"
|
||||
fi
|
||||
|
||||
echo "tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate stable semver tag
|
||||
@@ -46,7 +57,6 @@ jobs:
|
||||
echo "publish=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "publish=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping non-stable semver tag ${{ steps.target.outputs.tag }}"
|
||||
|
||||
@@ -67,8 +77,6 @@ jobs:
|
||||
- name: Derive image metadata
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
id: meta
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.target.outputs.tag }}"
|
||||
VERSION="${VERSION#v}"
|
||||
@@ -105,7 +113,7 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push release image
|
||||
- name: Build and push legacy dashboard image
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -116,13 +124,284 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=ccs-dashboard
|
||||
org.opencontainers.image.description=CCS Dashboard container image
|
||||
org.opencontainers.image.description=CCS Dashboard container image (deprecated - migrate to ghcr.io/kaitranntt/ccs:latest)
|
||||
org.opencontainers.image.url=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
|
||||
org.opencontainers.image.revision=${{ steps.meta.outputs.revision }}
|
||||
annotations: |
|
||||
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
index:org.opencontainers.image.description=CCS Dashboard container image
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
index:org.opencontainers.image.description=CCS Dashboard container image (deprecated - migrate to ghcr.io/kaitranntt/ccs:latest)
|
||||
cache-from: type=gha,scope=dashboard
|
||||
cache-to: type=gha,mode=max,scope=dashboard
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 2: Integrated image matrix — minimal (ccs:latest) and full (ccs:full)
|
||||
# ---------------------------------------------------------------------------
|
||||
publish-integrated:
|
||||
name: Publish integrated image (${{ matrix.flavor }})
|
||||
if: ${{ github.event_name != 'release' || !github.event.release.prerelease }}
|
||||
runs-on: [self-hosted, linux, x64, cliproxy]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [minimal, full]
|
||||
|
||||
steps:
|
||||
- name: Resolve target tag
|
||||
id: target
|
||||
env:
|
||||
MANUAL_TAG: ${{ inputs.tag }}
|
||||
RELEASE_EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
|
||||
TARGET_TAG="${RELEASE_EVENT_TAG}"
|
||||
else
|
||||
TARGET_TAG="${MANUAL_TAG}"
|
||||
fi
|
||||
echo "tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate stable semver tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${{ steps.target.outputs.tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "publish=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "publish=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping non-stable semver tag ${{ steps.target.outputs.tag }}"
|
||||
|
||||
- name: Checkout release tag
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.target.outputs.tag }}
|
||||
|
||||
- name: Set up QEMU
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Derive image metadata
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
id: meta
|
||||
env:
|
||||
FLAVOR: ${{ matrix.flavor }}
|
||||
PROMOTE_TO_LATEST: ${{ inputs.promote_to_latest || 'false' }}
|
||||
run: |
|
||||
VERSION="${{ steps.target.outputs.tag }}"
|
||||
VERSION="${VERSION#v}"
|
||||
MINOR="${VERSION%.*}"
|
||||
MAJOR="${VERSION%%.*}"
|
||||
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
||||
IMAGE="ghcr.io/${OWNER_LOWER}/ccs"
|
||||
REVISION=$(git rev-parse HEAD)
|
||||
|
||||
# Determine tag prefix for the full flavor
|
||||
if [[ "${FLAVOR}" == "full" ]]; then
|
||||
PREFIX="full-"
|
||||
MUTABLE_TAG="${IMAGE}:full"
|
||||
else
|
||||
PREFIX=""
|
||||
MUTABLE_TAG="${IMAGE}:latest"
|
||||
fi
|
||||
|
||||
# Immutable version-pinned tag is always published
|
||||
TAGS="${IMAGE}:${PREFIX}${VERSION}"
|
||||
|
||||
# Mutable tags (:latest / :full / major / minor aliases) are published only when:
|
||||
# - triggered by a GitHub release event (not a prerelease), OR
|
||||
# - workflow_dispatch with promote_to_latest=true
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "release" || "${PROMOTE_TO_LATEST}" == "true" ]]; then
|
||||
TAGS="${TAGS}
|
||||
${IMAGE}:${PREFIX}${MINOR}
|
||||
${IMAGE}:${PREFIX}${MAJOR}
|
||||
${MUTABLE_TAG}"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "version=${VERSION}"
|
||||
echo "minor=${MINOR}"
|
||||
echo "major=${MAJOR}"
|
||||
echo "image=${IMAGE}"
|
||||
echo "revision=${REVISION}"
|
||||
echo "prefix=${PREFIX}"
|
||||
echo "tags<<EOF"
|
||||
echo "${TAGS}"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push integrated image (${{ matrix.flavor }})
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.integrated
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
FLAVOR=${{ matrix.flavor }}
|
||||
CCS_NPM_VERSION=${{ steps.meta.outputs.version }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=ccs
|
||||
org.opencontainers.image.description=CCS integrated image (${{ matrix.flavor }} flavor)
|
||||
org.opencontainers.image.url=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
|
||||
org.opencontainers.image.revision=${{ steps.meta.outputs.revision }}
|
||||
annotations: |
|
||||
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
index:org.opencontainers.image.description=CCS integrated image (${{ matrix.flavor }} flavor)
|
||||
cache-from: type=gha,scope=integrated-${{ matrix.flavor }}
|
||||
cache-to: type=gha,mode=max,scope=integrated-${{ matrix.flavor }}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 3: Post-publish smoke test — pull each variant and verify it boots
|
||||
# ---------------------------------------------------------------------------
|
||||
smoke-test:
|
||||
name: Smoke test ${{ matrix.image }}
|
||||
needs: [publish-integrated]
|
||||
if: ${{ github.event_name != 'release' || !github.event.release.prerelease }}
|
||||
runs-on: [self-hosted, linux, x64, cliproxy]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- flavor: minimal
|
||||
image_suffix: ""
|
||||
max_bytes: "367001600"
|
||||
- flavor: full
|
||||
image_suffix: "full-"
|
||||
max_bytes: "629145600"
|
||||
|
||||
steps:
|
||||
- name: Resolve target tag
|
||||
id: target
|
||||
env:
|
||||
MANUAL_TAG: ${{ inputs.tag }}
|
||||
RELEASE_EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
|
||||
TARGET_TAG="${RELEASE_EVENT_TAG}"
|
||||
else
|
||||
TARGET_TAG="${MANUAL_TAG}"
|
||||
fi
|
||||
echo "tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate stable semver tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${{ steps.target.outputs.tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "publish=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "publish=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout release tag (for test scripts)
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.target.outputs.tag }}
|
||||
|
||||
- name: Derive image reference
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
id: image
|
||||
env:
|
||||
IMAGE_SUFFIX: ${{ matrix.image_suffix }}
|
||||
run: |
|
||||
VERSION="${{ steps.target.outputs.tag }}"
|
||||
VERSION="${VERSION#v}"
|
||||
OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
||||
IMAGE_REF="ghcr.io/${OWNER_LOWER}/ccs:${IMAGE_SUFFIX}${VERSION}"
|
||||
echo "ref=${IMAGE_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
run: docker pull "${{ steps.image.outputs.ref }}"
|
||||
|
||||
- name: Assert image size budget
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
run: |
|
||||
chmod +x tests/docker/image-size.sh
|
||||
tests/docker/image-size.sh "${{ steps.image.outputs.ref }}" "${{ matrix.max_bytes }}"
|
||||
|
||||
- name: Boot container and wait for healthcheck
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
id: boot
|
||||
run: |
|
||||
CONTAINER_NAME="ccs-smoke-${{ matrix.flavor }}-${{ github.run_id }}"
|
||||
echo "container=${CONTAINER_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
docker run -d \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--rm \
|
||||
-p 13000:3000 \
|
||||
-p 18317:8317 \
|
||||
"${{ steps.image.outputs.ref }}"
|
||||
|
||||
echo "[i] Waiting for container healthcheck (up to 60s)..."
|
||||
for i in $(seq 1 12); do
|
||||
STATUS=$(docker inspect "${CONTAINER_NAME}" --format='{{.State.Health.Status}}' 2>/dev/null || echo "missing")
|
||||
if [[ "${STATUS}" == "healthy" ]]; then
|
||||
echo "[OK] Container is healthy after $((i * 5))s"
|
||||
break
|
||||
fi
|
||||
if [[ "${STATUS}" == "unhealthy" ]]; then
|
||||
echo "[X] Container marked unhealthy"
|
||||
docker logs "${CONTAINER_NAME}" --tail 50 >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " [${i}/12] status=${STATUS}, waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
- name: Probe dashboard port 3000
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
run: |
|
||||
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time 10 http://127.0.0.1:13000/ || echo "000")
|
||||
if [[ "${HTTP_CODE}" == "000" ]]; then
|
||||
echo "[X] Could not reach dashboard on :3000 (connection failed)"
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Dashboard port 3000 responded with HTTP ${HTTP_CODE}"
|
||||
|
||||
- name: Probe CLIProxy port 8317
|
||||
if: steps.tag.outputs.publish == 'true'
|
||||
run: |
|
||||
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time 10 http://127.0.0.1:18317/ || echo "000")
|
||||
if [[ "${HTTP_CODE}" == "000" ]]; then
|
||||
echo "[X] Could not reach CLIProxy on :8317 (connection failed)"
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] CLIProxy port 8317 responded with HTTP ${HTTP_CODE}"
|
||||
|
||||
- name: Stop smoke test container
|
||||
if: always() && steps.tag.outputs.publish == 'true'
|
||||
run: |
|
||||
docker stop "ccs-smoke-${{ matrix.flavor }}-${{ github.run_id }}" 2>/dev/null || true
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Deprecated
|
||||
|
||||
* **docker:** `ghcr.io/kaitranntt/ccs-dashboard:latest` Docker image is deprecated — migrate to `ghcr.io/kaitranntt/ccs:latest` (minimal, CCS + CLIProxy) or `ghcr.io/kaitranntt/ccs:full` (with claude-code, gemini-cli, grok-cli, opencode). The legacy image continues publishing for 2 more releases and emits a startup warning. See [#1251](https://github.com/kaitranntt/ccs/issues/1251).
|
||||
|
||||
## [7.79.1](https://github.com/kaitranntt/ccs/compare/v7.79.0...v7.79.1) (2026-05-14)
|
||||
|
||||
### Hotfixes
|
||||
|
||||
@@ -20,6 +20,8 @@ Anthropic-compatible APIs without config thrash.
|
||||
|
||||
</div>
|
||||
|
||||
> **[Docker]** `ghcr.io/kaitranntt/ccs-dashboard:latest` is deprecated. Use `ghcr.io/kaitranntt/ccs:latest` instead. See [#1251](https://github.com/kaitranntt/ccs/issues/1251) and [docker/README.md](docker/README.md#choosing-an-image) for migration details.
|
||||
|
||||
## Why CCS
|
||||
|
||||
CCS gives you one stable command surface while letting you switch between:
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
FROM eceasy/cli-proxy-api:latest
|
||||
|
||||
ARG CCS_NPM_VERSION=latest
|
||||
# FLAVOR=minimal installs CCS only; FLAVOR=full adds claude-code, gemini-cli, grok-cli, opencode
|
||||
ARG FLAVOR=minimal
|
||||
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
curl \
|
||||
jq \
|
||||
nodejs \
|
||||
npm \
|
||||
supervisor
|
||||
|
||||
RUN npm install -g @kaitranntt/ccs@${CCS_NPM_VERSION} \
|
||||
# Install CCS CLI (always present in both flavors)
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm install -g @kaitranntt/ccs@${CCS_NPM_VERSION} \
|
||||
&& ln -sf /usr/local/lib/node_modules/@kaitranntt/ccs/dist/docker/docker-bootstrap.js /usr/local/bin/ccs-docker-bootstrap
|
||||
|
||||
# Install AI CLIs only in the full flavor (all 4 bundled as one layer)
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
if [ "$FLAVOR" = "full" ]; then \
|
||||
npm install -g --ignore-scripts \
|
||||
@anthropic-ai/claude-code \
|
||||
@google/gemini-cli \
|
||||
@vibe-kit/grok-cli \
|
||||
&& curl -fsSL https://opencode.ai/install | sh -s -- --no-modify-path 2>/dev/null || true; \
|
||||
fi
|
||||
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
COPY entrypoint-integrated.sh /entrypoint-integrated.sh
|
||||
|
||||
|
||||
+33
-9
@@ -13,6 +13,16 @@ Persistent config, restart on reboot.
|
||||
|
||||
<br>
|
||||
|
||||
## Choosing an image
|
||||
|
||||
| Tag | Use | Approx. size | Status |
|
||||
|---|---|---|---|
|
||||
| `ghcr.io/kaitranntt/ccs:latest` | CCS + CLIProxy, no AI CLIs pre-installed | < 350 MB | **Recommended** |
|
||||
| `ghcr.io/kaitranntt/ccs:full` | CCS + CLIProxy + claude-code + gemini-cli + grok-cli + opencode | < 600 MB | Supported |
|
||||
| `ghcr.io/kaitranntt/ccs-dashboard:latest` | Legacy all-in-one image | > 600 MB | **Deprecated** — migrate to `ccs:latest`. Sunset after 2 releases. See [#1251](https://github.com/kaitranntt/ccs/issues/1251) |
|
||||
|
||||
Both `ccs:latest` and `ccs:full` also publish pinned version tags (`ccs:<major>.<minor>.<patch>`, `ccs:<major>.<minor>`, `ccs:<major>`) for reproducible deployments. The `:full` variants carry the `full-` prefix: `ccs:full-<ver>`, `ccs:full-<minor>`, etc.
|
||||
|
||||
## Preferred: `ccs docker`
|
||||
|
||||
The CLI now ships a first-class Docker command suite for the integrated CCS + CLIProxy stack:
|
||||
@@ -147,24 +157,38 @@ Expected healthy output:
|
||||
|
||||
## Prebuilt Image Quick Start
|
||||
|
||||
This existing image still runs the CCS dashboard and its locally managed CLIProxy inside one
|
||||
container. It does not provide the remote staging and in-container self-update flow exposed by
|
||||
`ccs docker`.
|
||||
|
||||
Pull the latest stable release image from GitHub Container Registry:
|
||||
Pull the recommended minimal image (CCS + CLIProxy, no AI CLIs):
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name ccs-dashboard \
|
||||
--name ccs \
|
||||
--restart unless-stopped \
|
||||
-p 3000:3000 \
|
||||
-p 8317:8317 \
|
||||
-e CCS_PORT=3000 \
|
||||
-v ccs_home:/home/node/.ccs \
|
||||
ghcr.io/kaitranntt/ccs-dashboard:latest
|
||||
-v ccs_home:/root/.ccs \
|
||||
ghcr.io/kaitranntt/ccs:latest
|
||||
```
|
||||
|
||||
Release-tag images are also published as `ghcr.io/kaitranntt/ccs-dashboard:<version>`.
|
||||
Or pull the full image with all 4 AI CLIs pre-installed:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name ccs \
|
||||
--restart unless-stopped \
|
||||
-p 3000:3000 \
|
||||
-p 8317:8317 \
|
||||
-e CCS_PORT=3000 \
|
||||
-v ccs_home:/root/.ccs \
|
||||
ghcr.io/kaitranntt/ccs:full
|
||||
```
|
||||
|
||||
Release-tag images are published as `ghcr.io/kaitranntt/ccs:<version>` (minimal) and `ghcr.io/kaitranntt/ccs:full-<version>` (full).
|
||||
|
||||
### Legacy image (deprecated)
|
||||
|
||||
The `ghcr.io/kaitranntt/ccs-dashboard:latest` image continues building for 2 more releases but
|
||||
emits a deprecation warning on startup. Migrate to `ccs:latest` at your earliest convenience.
|
||||
|
||||
## Prebuilt Image Build Locally
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[WARN] ghcr.io/kaitranntt/ccs-dashboard is deprecated. Migrate to ghcr.io/kaitranntt/ccs:latest. See https://github.com/kaitranntt/ccs/issues/1251" >&2
|
||||
|
||||
ccs_home_dir="${CCS_HOME_DIR:-/home/node/.ccs}"
|
||||
|
||||
mkdir -p "$ccs_home_dir"
|
||||
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit tests for image-size.sh pass/fail logic using mock docker output.
|
||||
# Does NOT require a real Docker daemon or pulled images.
|
||||
# Run: bash tests/docker/image-size-logic.test.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT="${SCRIPT_DIR}/image-size.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
run_test() {
|
||||
local name="$1"
|
||||
local expected_exit="$2"
|
||||
shift 2
|
||||
# Remaining args: env vars to set before calling the script
|
||||
local actual_exit=0
|
||||
(
|
||||
# Override docker with a mock that returns a fixed size
|
||||
eval "$@"
|
||||
bash "$SCRIPT" "mock-image:tag" "$MAX_BYTES" > /dev/null 2>&1
|
||||
) || actual_exit=$?
|
||||
|
||||
if [[ "$actual_exit" -eq "$expected_exit" ]]; then
|
||||
echo "[OK] ${name}"
|
||||
(( PASS++ )) || true
|
||||
else
|
||||
echo "[X] ${name}: expected exit ${expected_exit}, got ${actual_exit}"
|
||||
(( FAIL++ )) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# We mock docker by injecting a wrapper into PATH that echoes a fixed
|
||||
# size value for `docker image inspect` and pretends inspect succeeds.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
MOCK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$MOCK_DIR"' EXIT
|
||||
|
||||
make_mock_docker() {
|
||||
local size="$1"
|
||||
cat > "${MOCK_DIR}/docker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
# Mock docker for image-size.sh tests
|
||||
if [[ "\$1" == "image" && "\$2" == "inspect" ]]; then
|
||||
echo "${size}"
|
||||
exit 0
|
||||
fi
|
||||
# pull / other sub-commands: succeed silently
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "${MOCK_DIR}/docker"
|
||||
}
|
||||
|
||||
run_mock_test() {
|
||||
local name="$1"
|
||||
local expected_exit="$2"
|
||||
local mock_size="$3"
|
||||
local max_bytes="$4"
|
||||
|
||||
make_mock_docker "$mock_size"
|
||||
|
||||
local actual_exit=0
|
||||
PATH="${MOCK_DIR}:${PATH}" bash "$SCRIPT" "mock-image:tag" "$max_bytes" > /dev/null 2>&1 || actual_exit=$?
|
||||
|
||||
if [[ "$actual_exit" -eq "$expected_exit" ]]; then
|
||||
echo "[OK] ${name}"
|
||||
(( PASS++ )) || true
|
||||
else
|
||||
echo "[X] ${name}: expected exit ${expected_exit}, got ${actual_exit}"
|
||||
(( FAIL++ )) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tests
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Running image-size.sh unit tests..."
|
||||
echo ""
|
||||
|
||||
# Pass: actual < budget
|
||||
run_mock_test "pass when actual size < budget" 0 \
|
||||
"300000000" "367001600"
|
||||
|
||||
# Pass: actual == budget (boundary)
|
||||
run_mock_test "pass when actual size == budget (boundary)" 0 \
|
||||
"367001600" "367001600"
|
||||
|
||||
# Fail: actual > budget by 1 byte
|
||||
run_mock_test "fail when actual size exceeds budget by 1 byte" 1 \
|
||||
"367001601" "367001600"
|
||||
|
||||
# Fail: actual is much larger than budget
|
||||
run_mock_test "fail when actual size greatly exceeds budget" 1 \
|
||||
"900000000" "629145600"
|
||||
|
||||
# Error: wrong arg count (no args)
|
||||
actual_exit=0
|
||||
bash "$SCRIPT" > /dev/null 2>&1 || actual_exit=$?
|
||||
if [[ "$actual_exit" -ne 0 ]]; then
|
||||
echo "[OK] fail when called with no args"
|
||||
(( PASS++ )) || true
|
||||
else
|
||||
echo "[X] fail when called with no args: expected non-zero exit"
|
||||
(( FAIL++ )) || true
|
||||
fi
|
||||
|
||||
# Error: non-integer max-bytes
|
||||
actual_exit=0
|
||||
PATH="${MOCK_DIR}:${PATH}" bash "$SCRIPT" "mock-image:tag" "not-a-number" > /dev/null 2>&1 || actual_exit=$?
|
||||
if [[ "$actual_exit" -ne 0 ]]; then
|
||||
echo "[OK] fail when max-bytes is not an integer"
|
||||
(( PASS++ )) || true
|
||||
else
|
||||
echo "[X] fail when max-bytes is not an integer: expected non-zero exit"
|
||||
(( FAIL++ )) || true
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Summary
|
||||
# ------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
echo ""
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Asserts that a Docker image does not exceed a given byte budget.
|
||||
#
|
||||
# Usage: image-size.sh <image:tag> <max-bytes>
|
||||
# Exit: 0 on pass, 1 on fail
|
||||
#
|
||||
# Examples:
|
||||
# image-size.sh ghcr.io/kaitranntt/ccs:latest 367001600 # 350 MB
|
||||
# image-size.sh ghcr.io/kaitranntt/ccs:full 629145600 # 600 MB
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "[X] Usage: $0 <image:tag> <max-bytes>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE="$1"
|
||||
MAX_BYTES="$2"
|
||||
|
||||
# Validate that max-bytes is a positive integer
|
||||
if ! [[ "$MAX_BYTES" =~ ^[0-9]+$ ]]; then
|
||||
echo "[X] max-bytes must be a positive integer, got: ${MAX_BYTES}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pull the image if not already present (allows use in a clean CI environment)
|
||||
if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then
|
||||
echo "[i] Pulling ${IMAGE}..." >&2
|
||||
docker pull "$IMAGE" >&2
|
||||
fi
|
||||
|
||||
ACTUAL_BYTES=$(docker image inspect "$IMAGE" --format='{{.Size}}' 2>/dev/null)
|
||||
|
||||
if [[ -z "$ACTUAL_BYTES" ]]; then
|
||||
echo "[X] Could not inspect image: ${IMAGE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTUAL_MB=$(( ACTUAL_BYTES / 1048576 ))
|
||||
MAX_MB=$(( MAX_BYTES / 1048576 ))
|
||||
|
||||
if (( ACTUAL_BYTES > MAX_BYTES )); then
|
||||
echo "[X] Image size check FAILED: ${IMAGE}" >&2
|
||||
echo " Actual: ${ACTUAL_BYTES} bytes (${ACTUAL_MB} MB)" >&2
|
||||
echo " Budget: ${MAX_BYTES} bytes (${MAX_MB} MB)" >&2
|
||||
echo " Excess: $(( ACTUAL_BYTES - MAX_BYTES )) bytes ($(( ACTUAL_MB - MAX_MB )) MB over budget)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[OK] Image size check PASSED: ${IMAGE}"
|
||||
echo " Actual: ${ACTUAL_BYTES} bytes (${ACTUAL_MB} MB)"
|
||||
echo " Budget: ${MAX_BYTES} bytes (${MAX_MB} MB)"
|
||||
echo " Margin: $(( MAX_BYTES - ACTUAL_BYTES )) bytes ($(( MAX_MB - ACTUAL_MB )) MB remaining)"
|
||||
Reference in New Issue
Block a user