Files
ccs/.github/workflows/breaking-change-guard.yml
T
Kai (Tam Nhu) TranandGitHub e7ce699dc2 fix(ci): reviewer loop 6 — REV12 compose image parsing, REV13 fork bypass, REV14 :full audit (#1278)
* fix(ci): breaking-change-guard handles \${VAR:-default} compose image syntax (REV12)

The sed 's/:.*//' pattern truncated at the first colon in the
\${CCS_IMAGE:-ghcr.io/...} expression, yielding "\${CCS_IMAGE" as the
image name instead of the actual registry path. Any change to the default
image namespace was therefore undetectable.

Introduce extract_image_name() that first strips the \${VAR:-default}
wrapper with a sed -E expression, then strips only the trailing :tag
suffix using a pattern that preserves internal colons (e.g. registry:5000/
owner/repo). Applied to both OLD_RAW and NEW_RAW extraction paths.

* fix(ci): breaking-change-guard runs on ubuntu-latest to cover forked PRs (REV13)

The trusted-author gate (COLLABORATOR|MEMBER|OWNER) caused forked-PR
contributors to bypass the breaking-change check entirely. A forked
contributor could rename services.ccs or change the image namespace
without a feat!/fix! marker and the guard would never run.

This workflow is a documented exception to the self-hosted-first policy:
it performs ONLY pure YAML diff parsing (git show / awk / sed). No build,
install, or arbitrary PR-branch scripts are executed. The checkout uses
persist-credentials: false. There is no untrusted code execution, so
ubuntu-latest is safe and necessary for universal fork coverage.

Update self-hosted-runner-policy.test.ts to:
- Introduce GITHUB_HOSTED_RUNNER_EXCEPTIONS registry with required
  justification comments for each entry
- Skip exception workflows in the "keeps active workflows on local runners"
  and "gates pull-request workflows" assertions
- Add a new "documented exceptions use github-hosted runners" test that
  verifies each exception entry actually uses a GitHub-hosted runner
  (prevents stale entries accumulating without cleanup)
2026-05-17 06:01:25 -04:00

165 lines
7.6 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
# Exception to self-hosted-first policy (documented in CLAUDE.md "Self-Hosted Runner Policy"):
# This workflow MUST cover ALL PRs including forks — a forked contributor can rename
# services.ccs or change the image namespace without a breaking-change marker, which
# silently breaks sibling-container setups for every user. Gating on trusted-author
# association would let forked PRs bypass the check entirely.
#
# Safety justification: this workflow does ONLY pure YAML diff parsing (git show / git diff
# + shell + awk). It checks out the code with persist-credentials: false and runs no build,
# install, or arbitrary scripts from the PR branch. There is no untrusted code execution,
# so running on a GitHub-hosted runner is safe and required for universal coverage.
runs-on: ubuntu-latest
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)
#
# compose.yaml uses ${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}. A naive
# sed 's/:.*//' truncates at the FIRST colon, yielding "${CCS_IMAGE" instead
# of the actual image name. We strip the ${VAR:-default} wrapper first, then
# strip only the trailing :tag suffix (preserving internal colons such as
# those in registry:port/owner/repo).
extract_image_name() {
# $1 = raw image line content (everything after "image: ")
local raw="$1"
# Strip ${VAR:-default} wrapper if present
raw=$(printf '%s' "$raw" | sed -E 's/^\$\{[A-Za-z_][A-Za-z0-9_]*:-//; s/\}$//')
# Strip only the trailing :tag — preserve internal colons (e.g. registry:5000/owner/repo)
printf '%s' "$raw" | sed 's|:[^:/]*$||'
}
OLD_RAW=$(git show "${BASE}:docker/compose.yaml" \
| grep -m1 '^\s*image:' | sed 's/.*image:\s*//' | tr -d ' ')
NEW_RAW=$(grep -m1 '^\s*image:' docker/compose.yaml \
| sed 's/.*image:\s*//' | tr -d ' ')
OLD_IMAGE=$(extract_image_name "$OLD_RAW")
NEW_IMAGE=$(extract_image_name "$NEW_RAW")
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 contract — Docker DNS on ccs-net uses the compose service
# KEY as the hostname (e.g. http://ccs:8317). Renaming or removing
# "ccs:" breaks every sibling container in the wild. Adding new service
# keys (e.g. a sidecar) does NOT affect the "ccs" DNS contract and is
# therefore allowed without a breaking-change marker.
#
# 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.
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
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"