feat(docker): publish ccs:latest + ccs:full integrated images (P1 of #1251) (#1257)

* 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:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-16 12:33:42 -04:00
1 parent 423708f28f
commit d558cd2e36
8 files changed
+534 -21

No files matched your search

+132
View File
@@ -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
+53
View File
@@ -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)"