mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
* 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.
148 lines
6.5 KiB
YAML
148 lines
6.5 KiB
YAML
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"
|