* fix(docker): parameterize image with CCS_IMAGE env so smoke-test exercises new digest
docker/compose.yaml hardcoded image: ghcr.io/kaitranntt/ccs:latest, causing
network-contract.sh to always use the old published image regardless of what
IMAGE_OVERRIDE was passed. Switching to \${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}
lets CI pass CCS_IMAGE=<digest> so the smoke-test actually exercises the
just-built image. Default end-user behaviour is unchanged.
Resolves REV4 (reviewer loop 2, PR #1261).
* fix(test): image-size.sh fails loudly on manifest inspection failure
The --platform branch previously exited 0 when imagetools inspect returned
empty or zero bytes, silently passing the budget check. A broken image
could reach production undetected if buildx had any inspection issue.
Changed to exit 1 with a clear diagnostic message explaining possible causes
(old buildx, manifest format mismatch, image not yet pushed). No --allow-inspect-failure
escape hatch is provided; CI must fix the root cause.
Resolves REV5 (reviewer loop 2, PR #1261).
* test(docker): cover --platform branch in image-size-logic tests
Prior tests only exercised the local docker image inspect path. The
--platform branch (imagetools inspect) had zero coverage, meaning the
REV5 silent-pass regression would not have been caught by CI.
Added 5 mock-based test cases for the --platform branch:
- pass when platform-scoped compressed size < budget
- fail when platform-scoped compressed size > budget
- fail (exit 1) when imagetools inspect errors (REV5 guard)
- fail (exit 1) when reported size is "0" (REV5 guard)
- fail (exit 1) when size output is empty (REV5 guard)
Mocks follow the existing pattern (override docker in PATH with a temp
wrapper), extended to handle buildx imagetools inspect subcommand.
Resolves REV6 (reviewer loop 2, PR #1261).
* fix(test): compose-parity image_name() handles \${VAR:-default} syntax
After parameterizing compose.yaml's image field with \${CCS_IMAGE:-...},
the image_name() sed pipeline cut at the first colon in the shell variable
syntax (:-) instead of the tag separator, returning '\${CCS_IMAGE' and
failing the kaitranntt/ccs grep.
Added a sed step to unwrap \${VAR:-default} by extracting just the default
value before stripping the tag. Existing plain image: name:tag references
are unaffected.
* fix(ci): breaking-change-guard skips missing base files (CI-1)
Guard each git-show call with git cat-file -e existence check before
attempting to read docker/compose.yaml from the base branch. When the
file doesn't exist on the base (new file in this PR), the script would
crash with "fatal: path exists on disk but not in origin/dev". New
files can't cause a contract regression, so we exit early with
breaking=0.
* style: prettier reformat unrelated drift (CI-2)
Two files had minor formatting drift from earlier umbrella PRs:
- src/cliproxy/quota/quota-manager.ts
- src/management/checks/image-analysis-check.ts
No logic changes — formatter-only pass to unblock CI format:check.
* fix(test): update trusted-author gate count to 4 in ci-workflow test (CI-3)
PR #1260 added a compose-parity job to ci.yml. That job runs on
self-hosted runners using PR-provided checkout, so it legitimately
requires the trusted-author guard (same as validate, build, test).
The test expected 3 occurrences; the correct count is now 4:
validate (matrix), build, test, compose-parity.
* fix(ci): smoke-test passes compose path + image ref to network-contract (REV-1)
network-contract.sh signature is: <compose-file> [image-ref]
The smoke-test job was calling it as:
bash tests/docker/network-contract.sh "${{ steps.image.outputs.ref }}"
which placed the image ref in the compose-file position ($1), causing
the script to try docker compose -f <image-ref> which fails.
Corrected to:
bash tests/docker/network-contract.sh docker/compose.yaml "${{ steps.image.outputs.ref }}"
Also removes publish-dashboard from smoke-test.needs (REV-2): when
publish-dashboard is SKIPPED on prerelease events, GitHub Actions
propagates the skip to downstream jobs, so smoke-test and
promote-mutable-tags were silently skipped on every rc.N publish.
smoke-test only verifies the integrated image; it has no dependency
on the legacy dashboard image job.
* docs(docker): annotate /root/.ccs path in compose volume (REV-3 clarification)
The reviewer raised a concern that the compose volume mounts /root/.ccs
but the entrypoint might default to /home/node/.ccs. This is a false
positive: the integrated image uses entrypoint-integrated.sh (not
entrypoint.sh), which runs under supervisord with user=root and
explicitly mkdir -p /root/.ccs. HOME is /root inside the container.
The volume mount at /root/.ccs is correct.
Added an inline comment documenting the reasoning so future reviewers
do not confuse entrypoint.sh (legacy dashboard image) with
entrypoint-integrated.sh (integrated image).
* fix(ci): gate docs-parity pull_request job to trusted authors
docs-parity.yml runs on a self-hosted runner and checks out PR code.
The self-hosted-runner-policy test requires any such workflow to include
the trusted-author guard. The workflow was missing the guard, causing
bun test:fast to fail with 1 failure.
Allow push events (no author check needed — push is to own branch)
and trusted-contributor PRs only.
* docs: hoist Docker zero-install quickstart above npm install path
- Create docs/quickstart-snippet.md as canonical source for the
two-command flow (curl + docker compose up -d), wrapped in
<!-- quickstart-snippet-start/end --> markers
- Hoist the snippet into README.md immediately below the deprecation
banner, above all other install paths
- Rename old npm-only "## Quick Start" to "## Install on Host (npm)"
and move it below the Docker quickstart
* docs(docker): restructure README with zero-install first and migration section
- Reorder top-level sections: zero-install (canonical snippet with
markers), choosing an image, power-user ccs docker, prebuilt image,
connect your app to CLIProxy, migration, env vars, troubleshooting
- Add deprecation banner at the top pointing at the migration section
- Add ## Migration from ccs-dashboard:latest section with step-by-step
instructions covering compose down, data preservation, named volume
vs bind-mount path, and compose up with the new image
- Keep P1's Choosing an image table and P5's Connect Your App to
CLIProxy section intact, just repositioned
* test(docs): parity check for quickstart snippet across README files
Assert README.md and docker/README.md both contain the canonical
quickstart block verbatim, anchored by marker comments. Exits non-zero
and prints a diff on any drift.
* ci(docs): wire quickstart-parity test on push and PR
Runs tests/docs/quickstart-parity.sh on self-hosted runner whenever
docs/quickstart-snippet.md, README.md, docker/README.md, or the
test/workflow files themselves change. Fails fast on snippet drift.
Add "Connect your app to CLIProxy" section to docker/README.md with:
- Public contract table (network=ccs-net, service DNS=ccs, ports 8317/3000)
- Pattern A: same compose file with external network reference
- Pattern B: docker run --network ccs-net
- Troubleshooting subsection (DNS, missing network, conflict, Podman, MTU)
Add one-line link in README.md pointing to the new section.
Add CHANGELOG entry under Unreleased noting the contract as SemVer-major stable.
Add CONTRIBUTING.md note that changing services.ccs or networks.ccs-net requires major bump.
- Build stage keeps Bun for fast installs; appends `npm install --package-lock-only`
to generate an ephemeral package-lock.json immediately after `bun run build:all`.
- Runtime stage removes BUN_VERSION ARG/ENV, the bun.sh curl install, and
`bun install --frozen-lockfile --production`. Replaces with:
COPY --from=build /app/package.json /app/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts
- --ignore-scripts rationale: postinstall (scripts/postinstall.js) writes ~/.ccs/
config — not needed in Docker context. bcrypt v6+ is pure-JS, no native compile.
- package-lock.json already in .gitignore (line 33); never committed.
- docker/Dockerfile.integrated unchanged — no Bun present there (alpine + npm).
- Targets: image size reduction >= 300 MB by eliminating ~130 MB Bun binary + installer.
* 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).
Replace direct volume mountpoint writes (requires root) with docker cp
which works without elevated permissions. Fix malformed npx bcrypt
command — use require('bcrypt') with Node module resolution. Add
security note about not committing password hashes to docker-compose.
- Add test assertion for --host auth reminder message
- Use Node module resolution for bcrypt in README (not hardcoded path)
- Replace cat|grep with direct grep in verification commands
Users deploying via ccs docker up hit silent failures when accessing
the dashboard remotely: empty providers, wrong version badge (v5.0.0),
no CLIProxy detected. Root cause is the dashboard auth middleware
blocking non-localhost API access.
Added sections:
- Dashboard auth setup (required for remote access)
- Auth token migration from previous deployments
- Post-deployment verification checklist
- Troubleshooting: empty dashboard, 0 clients, ETXTBSY race
Closes#841
- document the new ccs docker command suite and remote host staging path
- clarify how the existing prebuilt image differs from the integrated CLI flow
- record issue #812 in the project roadmap
Refs #812