From a4e16e0b09647392434e98837b8866b1f27eaaaf Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sun, 17 May 2026 05:12:04 -0400 Subject: [PATCH] fix: tighten parity check + catch service-key rename (#1261 loop 3) (#1274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(test): tighten compose-parity image-name match to exact expected values (REV7) Replace loose substring grep (grep -q "ccs") with exact equality checks against declared EXPECTED_CANONICAL_IMAGE and EXPECTED_INTEGRATED_IMAGE constants. The integrated compose builds locally as ccs-cliproxy:latest (not ghcr.io/kaitranntt/ccs), so both expected names are explicitly documented at the top of the assertion block. Fixes: a drift in integrated compose to a wrong owner/registry could slip through the old "grep -q ccs" check; now any name other than the declared constant is a hard failure. * feat(ci): breaking-change-guard catches services.ccs rename — public DNS contract (REV8) Docker's service-name DNS uses the compose service KEY as the hostname. Sibling containers on ccs-net reach CCS via http://ccs:8317; renaming services.ccs: to anything else silently breaks that contract even when image name, network name, and container_name are unchanged. Add check 4 to the guard: - Extract top-level service keys from both base and HEAD versions of docker/compose.yaml using awk (no external YAML parser required). - Fail if the "ccs" key is absent from HEAD. - Fail if the sorted set of service keys differs from base. Wraps inside the existing git cat-file guard so new-file PRs skip it. --- .github/workflows/breaking-change-guard.yml | 25 +++++++++++++++++ tests/docker/compose-parity.sh | 30 ++++++++++++++++----- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.github/workflows/breaking-change-guard.yml b/.github/workflows/breaking-change-guard.yml index 6c3984fc..96ebe620 100644 --- a/.github/workflows/breaking-change-guard.yml +++ b/.github/workflows/breaking-change-guard.yml @@ -7,6 +7,7 @@ name: Breaking Change Guard – Docker Compose Contract # services.ccs.image (the image *name*, not the tag) # networks.ccs-net.name # services.ccs.container_name (if set) +# services keys (Docker DNS uses the service key; renaming "ccs:" changes http://ccs:8317) # # If any of these fields change in a PR, the workflow fails unless at least # one commit in the PR includes a breaking-change marker (feat!: or fix!:). @@ -80,6 +81,30 @@ jobs: BREAKING=1 fi + # 4. Service key rename — public DNS contract on ccs-net depends on the + # service key being "ccs". Docker's service-name DNS resolves sibling + # containers via the compose service KEY (e.g. http://ccs:8317). A + # rename from "ccs:" to anything else silently breaks every sibling + # container in the wild even when image/network/container_name match. + # + # Extraction logic: find lines that look like top-level service keys + # (two-space indent + identifier + colon, NOT sub-keys like "image:"). + # This matches YAML service keys without requiring an external YAML parser. + OLD_KEYS=$(git show "${BASE}:docker/compose.yaml" 2>/dev/null \ + | awk '/^services:/{s=1;next} s && /^ [a-zA-Z0-9_-]+:/{print $1} /^[^ ]/{s=0}' \ + | tr -d ':' | sort | tr '\n' ' ' | sed 's/ $//') + NEW_KEYS=$(awk '/^services:/{s=1;next} s && /^ [a-zA-Z0-9_-]+:/{print $1} /^[^ ]/{s=0}' \ + docker/compose.yaml \ + | tr -d ':' | sort | tr '\n' ' ' | sed 's/ $//') + if ! echo " ${NEW_KEYS} " | grep -q " ccs "; then + echo "[!] BREAKING: services.ccs key missing from docker/compose.yaml — sibling containers on ccs-net rely on DNS hostname 'ccs'" + BREAKING=1 + fi + if [[ "${OLD_KEYS}" != "${NEW_KEYS}" ]]; then + echo "[!] BREAKING: services keys changed: '${OLD_KEYS}' -> '${NEW_KEYS}'" + BREAKING=1 + fi + echo "breaking=${BREAKING}" >> "$GITHUB_OUTPUT" - name: Require breaking-change commit marker if contract changed diff --git a/tests/docker/compose-parity.sh b/tests/docker/compose-parity.sh index 9d1af093..d94d83a5 100644 --- a/tests/docker/compose-parity.sh +++ b/tests/docker/compose-parity.sh @@ -65,19 +65,37 @@ volume_targets() { } # --------------------------------------------------------------------------- -# 1. Image name (repo without tag) — both should reference kaitranntt/ccs +# Expected image names (without tag) — source of truth for this assertion. +# +# canonical (docker/compose.yaml): +# Pulls from the public registry. Default image is ghcr.io/kaitranntt/ccs. +# +# integrated (docker/docker-compose.integrated.yml): +# Built locally from Dockerfile.integrated; the resulting image is tagged +# ccs-cliproxy (no registry prefix) so it stays separate from the public +# image but is still clearly a CCS-family image. +# --------------------------------------------------------------------------- +EXPECTED_CANONICAL_IMAGE="ghcr.io/kaitranntt/ccs" +EXPECTED_INTEGRATED_IMAGE="ccs-cliproxy" + +# --------------------------------------------------------------------------- +# 1. Image name (repo without tag) — assert exact match against expected names # --------------------------------------------------------------------------- log "Checking image name parity..." CANONICAL_IMAGE=$(image_name "$CANONICAL" "ccs") INTEGRATED_IMAGE=$(image_name "$INTEGRATED" "ccs-cliproxy") -# Both must contain kaitranntt/ccs (integrated builds locally but from the same Dockerfile) -if echo "$CANONICAL_IMAGE" | grep -q "kaitranntt/ccs" && \ - echo "$INTEGRATED_IMAGE" | grep -q "ccs"; then - ok "Image names reference expected repo (canonical: ${CANONICAL_IMAGE}, integrated: ${INTEGRATED_IMAGE})" +if [[ "${CANONICAL_IMAGE}" != "${EXPECTED_CANONICAL_IMAGE}" ]]; then + fail "Canonical image name mismatch — expected='${EXPECTED_CANONICAL_IMAGE}' got='${CANONICAL_IMAGE}'" else - fail "Image name mismatch — canonical='${CANONICAL_IMAGE}' integrated='${INTEGRATED_IMAGE}'" + ok "Canonical image name matches expected (${CANONICAL_IMAGE})" +fi + +if [[ "${INTEGRATED_IMAGE}" != "${EXPECTED_INTEGRATED_IMAGE}" ]]; then + fail "Integrated image name mismatch — expected='${EXPECTED_INTEGRATED_IMAGE}' got='${INTEGRATED_IMAGE}'" +else + ok "Integrated image name matches expected (${INTEGRATED_IMAGE})" fi # ---------------------------------------------------------------------------