mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-20 00:17:38 +00:00
* 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)
165 lines
7.6 KiB
YAML
165 lines
7.6 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
|
||
# 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"
|