Files
ccs/.github/workflows/breaking-change-guard.yml
T
Kai (Tam Nhu) Tran a4e16e0b09 fix: tighten parity check + catch service-key rename (#1261 loop 3) (#1274)
* 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.
2026-05-17 05:12:04 -04:00

148 lines
6.5 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Breaking Change Guard – Docker Compose Contract
# Guards against accidental breaking changes to the stable ccs-net contract
# defined in docker/compose.yaml. The following fields are considered stable
# API — changing them breaks existing sibling-container setups:
#
# 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!:).
# This enforces a deliberate, visible decision to change the contract.
on:
pull_request:
branches: [main, dev]
paths:
- "docker/compose.yaml"
jobs:
guard:
name: Verify breaking changes are intentional
if: >-
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
runs-on: [self-hosted, linux, x64, cliproxy]
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }} --depth=50
- name: Check for contract-breaking changes
id: contract
run: |
set -euo pipefail
BASE="origin/${{ github.base_ref }}"
# Guard: if docker/compose.yaml does not exist in the base branch, this
# is a new file — no contract existed before, so no regression is possible.
if ! git cat-file -e "${BASE}:docker/compose.yaml" 2>/dev/null; then
echo "[i] docker/compose.yaml is new in this PR (not present on ${BASE}) — skipping contract check"
echo "breaking=0" >> "$GITHUB_OUTPUT"
exit 0
fi
BREAKING=0
# 1. Image name change (repo path, not tag — tags change every release)
OLD_IMAGE=$(git show "${BASE}:docker/compose.yaml" \
| grep -m1 '^\s*image:' | sed 's/.*image:\s*//' | sed 's/:.*//' | tr -d ' ')
NEW_IMAGE=$(grep -m1 '^\s*image:' docker/compose.yaml \
| sed 's/.*image:\s*//' | sed 's/:.*//' | tr -d ' ')
if [[ "${OLD_IMAGE}" != "${NEW_IMAGE}" ]]; then
echo "[!] BREAKING: image name changed: '${OLD_IMAGE}' -> '${NEW_IMAGE}'"
BREAKING=1
fi
# 2. ccs-net network name change
OLD_NET=$(git show "${BASE}:docker/compose.yaml" \
| grep 'name: ccs-net' | tr -d ' ' || echo "")
NEW_NET=$(grep 'name: ccs-net' docker/compose.yaml | tr -d ' ' || echo "")
if [[ "${OLD_NET}" != "${NEW_NET}" ]]; then
echo "[!] BREAKING: ccs-net network name changed"
BREAKING=1
fi
# 3. container_name change (if present in either version)
OLD_CN=$(git show "${BASE}:docker/compose.yaml" \
| grep 'container_name:' | tr -d ' ' || echo "")
NEW_CN=$(grep 'container_name:' docker/compose.yaml | tr -d ' ' || echo "")
if [[ "${OLD_CN}" != "${NEW_CN}" ]]; then
echo "[!] BREAKING: container_name changed: '${OLD_CN}' -> '${NEW_CN}'"
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
if: steps.contract.outputs.breaking == '1'
run: |
set -euo pipefail
BASE="origin/${{ github.base_ref }}"
# Check all commit messages in the PR for feat! or fix! marker
HAS_BREAKING_MARKER=$(
git log "${BASE}"...HEAD --format="%s" \
| grep -cE "^(feat|fix)(\([^)]+\))?!:" || true
)
if [[ "${HAS_BREAKING_MARKER}" -eq 0 ]]; then
echo ""
echo "[X] Breaking change guard FAILED"
echo ""
echo " docker/compose.yaml was modified in a way that changes the stable"
echo " ccs-net contract (image name, network name, or container_name)."
echo ""
echo " These fields are relied upon by sibling containers. Changing them"
echo " without a breaking-change marker is a silent breaking change."
echo ""
echo " To proceed, rename at least one commit to use the feat! or fix!"
echo " breaking-change format:"
echo ""
echo " feat!: rename ccs service image to ghcr.io/owner/newname"
echo " fix!: align container_name with new naming convention"
echo ""
echo " See: https://www.conventionalcommits.org/en/v1.0.0/#specification"
exit 1
fi
echo "[OK] Breaking-change commit marker found — contract change is intentional"
- name: No contract-breaking changes detected
if: steps.contract.outputs.breaking == '0'
run: echo "[OK] No contract-breaking changes in docker/compose.yaml"