The mode-gate check was looking for ${REPO_ROOT}/internal/objectstorage
but the actual Go package lives at products/catalyst/bootstrap/api/internal/objectstorage.
Update the path so hard-fail mode auto-engages on this repo.
Validation:
bash scripts/check-vendor-coupling.sh
-> HARD-FAIL mode banner emitted, exit 0 on clean tree
Synthetic 'hetzner-object-storage' under platform/ -> exit 1.
Refs: PR #437 (#383) which surfaced the bug.
Co-authored-by: hatiyildiz <hatiyildiz@noreply.github.com>
309 lines
10 KiB
Bash
Executable File
309 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-vendor-coupling.sh — CI guardrail enforcing the founder's
|
|
# 2026-05-01 vendor-agnostic rule (canonical-seam map at
|
|
# docs/omantel-handover-wbs.md §3a, principle in
|
|
# docs/INVIOLABLE-PRINCIPLES.md #4).
|
|
#
|
|
# What this does:
|
|
# Scans platform/, clusters/, and products/catalyst/bootstrap/{api,ui}/
|
|
# for vendor-name leakage in places where a capability name belongs.
|
|
# The "vendor" set is hetzner|aws|gcp|azure|oci. The forbidden patterns:
|
|
#
|
|
# 1. "<vendor>-object-storage"
|
|
# — sealed-secret / config-map / overlay-secret name leaking the
|
|
# vendor. Capabilities should be named after their function
|
|
# (e.g. "object-storage-credentials"), not their provider.
|
|
#
|
|
# 2. "<chart>Overlay\.<vendor>\."
|
|
# — chart values block keyed to a specific vendor (e.g.
|
|
# veleroOverlay.hetzner.s3.accessKey). The overlay must key on
|
|
# the capability (veleroOverlay.objectStorage.s3.accessKey) and
|
|
# the per-vendor selection happens at the values-rendering layer.
|
|
#
|
|
# 3. "<vendor>ObjectStorage" (camelCase payload field)
|
|
# — Crossplane Composition forProvider key, Sovereign-API field,
|
|
# or wizard payload field that hardcodes the vendor.
|
|
#
|
|
# Allowed (excluded) paths — these are LEGITIMATELY per-provider:
|
|
#
|
|
# - infra/<provider>/ Tofu modules
|
|
# - internal/<provider>/ Provider-specific Go impls
|
|
# - internal/objectstorage/<provider>/ Post-#425 provider impls
|
|
# - core/pkg/<provider>/ Provider Go libs (if present)
|
|
# - any line containing "crossplane-contrib/provider-"
|
|
# Crossplane Provider CR refs
|
|
# - any *.md file Docs may discuss the rule
|
|
# - .git/ version-control internals
|
|
#
|
|
# Warn vs hard-fail mode:
|
|
#
|
|
# The vendor-rename work is happening across multiple PRs. While #425
|
|
# (the rename) is in flight, this script runs in WARN-ONLY mode so
|
|
# the work-in-progress drift is visible without blocking unrelated
|
|
# merges. Once #425 lands, the script flips to HARD-FAIL mode and any
|
|
# future re-introduction of vendor coupling fails CI.
|
|
#
|
|
# The mode gate is the existence of the internal/objectstorage/
|
|
# directory. Pre-#425 it does not exist → warnings only. Post-#425
|
|
# it exists → hard-fail.
|
|
#
|
|
# Exit codes:
|
|
# 0 — clean tree (or warn-only mode with warnings emitted to stderr)
|
|
# 1 — hard-fail mode AND violations found
|
|
# 2 — usage / I/O error
|
|
#
|
|
# Usage:
|
|
# scripts/check-vendor-coupling.sh
|
|
# scripts/check-vendor-coupling.sh --root /path/to/repo
|
|
# scripts/check-vendor-coupling.sh --force-hard-fail (testing)
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Args
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
FORCE_HARD_FAIL=0
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [--root DIR] [--force-hard-fail]
|
|
|
|
--root DIR Repo root to scan (default: ${REPO_ROOT})
|
|
--force-hard-fail Override mode gate, treat warnings as errors
|
|
(used by self-tests; CI never sets this)
|
|
-h, --help Show this message
|
|
|
|
See docs/omantel-handover-wbs.md §3a (canonical-seam map) and
|
|
docs/INVIOLABLE-PRINCIPLES.md #4 (no hardcoding) for the rule rationale.
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--root)
|
|
REPO_ROOT="$(cd "$2" && pwd)"
|
|
shift 2
|
|
;;
|
|
--force-hard-fail)
|
|
FORCE_HARD_FAIL=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -d "${REPO_ROOT}" ]]; then
|
|
echo "ERROR: repo root does not exist: ${REPO_ROOT}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mode gate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# The vendor-agnostic rename (#425) lands the canonical seam at
|
|
# internal/objectstorage/<provider>/. Once that directory exists, every
|
|
# remaining vendor-coupled reference is either drift or net-new — both
|
|
# must hard-fail. Until then, we can only warn (the drift IS the WIP).
|
|
HARD_FAIL=0
|
|
if [[ -d "${REPO_ROOT}/products/catalyst/bootstrap/api/internal/objectstorage" ]]; then
|
|
HARD_FAIL=1
|
|
fi
|
|
if [[ "${FORCE_HARD_FAIL}" -eq 1 ]]; then
|
|
HARD_FAIL=1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Patterns + scoped paths
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Vendors whose names should never leak into capability-named slots.
|
|
VENDORS=(hetzner aws gcp azure oci)
|
|
|
|
# Build an alternation regex for grep -E.
|
|
VENDOR_ALT="$(IFS='|'; echo "${VENDORS[*]}")"
|
|
|
|
# The forbidden patterns. grep -E syntax. Each is documented above.
|
|
PATTERNS=(
|
|
"(${VENDOR_ALT})-object-storage"
|
|
"[a-zA-Z0-9_]+Overlay\.(${VENDOR_ALT})\."
|
|
"(${VENDOR_ALT})ObjectStorage"
|
|
)
|
|
|
|
# Paths to scan. Vendor coupling is forbidden anywhere in these trees;
|
|
# legitimately per-vendor code lives elsewhere (see SCAN_EXCLUDES below).
|
|
SCAN_PATHS=(
|
|
"platform"
|
|
"clusters"
|
|
"products/catalyst/bootstrap/api"
|
|
"products/catalyst/bootstrap/ui"
|
|
)
|
|
|
|
# Paths excluded from the scan because they ARE legitimately per-provider.
|
|
# Matched against the file path relative to REPO_ROOT.
|
|
SCAN_EXCLUDES_REGEX='^(infra/[^/]+/|internal/objectstorage/[^/]+/|internal/[^/]+/|core/pkg/[^/]+/|\.git/)'
|
|
|
|
# Per-line content excludes — lines matching these are NEVER violations
|
|
# even inside scanned paths. (e.g. Crossplane Provider CR references that
|
|
# legitimately name `provider-hcloud`.)
|
|
LINE_EXCLUDES_REGEX='crossplane-contrib/provider-'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner() {
|
|
if [[ -t 1 ]]; then
|
|
printf '\033[1;36m== %s ==\033[0m\n' "$*"
|
|
else
|
|
printf '== %s ==\n' "$*"
|
|
fi
|
|
}
|
|
|
|
_warn() {
|
|
if [[ -t 2 ]]; then
|
|
printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2
|
|
else
|
|
printf 'WARN: %s\n' "$*" >&2
|
|
fi
|
|
}
|
|
|
|
_err() {
|
|
if [[ -t 2 ]]; then
|
|
printf '\033[1;31mFAIL:\033[0m %s\n' "$*" >&2
|
|
else
|
|
printf 'FAIL: %s\n' "$*" >&2
|
|
fi
|
|
}
|
|
|
|
_ok() {
|
|
if [[ -t 1 ]]; then
|
|
printf '\033[1;32mOK:\033[0m %s\n' "$*"
|
|
else
|
|
printf 'OK: %s\n' "$*"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scan
|
|
# ---------------------------------------------------------------------------
|
|
|
|
cd "${REPO_ROOT}"
|
|
|
|
if [[ "${HARD_FAIL}" -eq 1 ]]; then
|
|
_banner "vendor-coupling check (HARD-FAIL mode — internal/objectstorage/ present)"
|
|
else
|
|
_banner "vendor-coupling check (WARN-ONLY mode — pre-#425, internal/objectstorage/ absent)"
|
|
fi
|
|
|
|
# Build a single grep -E regex that ORs all patterns together. We pipe
|
|
# through filter passes (path exclude, content exclude, doc exclude).
|
|
JOINED_PATTERN="$(IFS='|'; echo "${PATTERNS[*]}")"
|
|
|
|
# Collect existing scan paths (some may not exist in a given checkout).
|
|
EXISTING_PATHS=()
|
|
for p in "${SCAN_PATHS[@]}"; do
|
|
if [[ -d "$p" ]]; then
|
|
EXISTING_PATHS+=("$p")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#EXISTING_PATHS[@]} -eq 0 ]]; then
|
|
_warn "none of the scan paths exist in this tree (${SCAN_PATHS[*]}) — nothing to check"
|
|
exit 0
|
|
fi
|
|
|
|
# grep flags:
|
|
# -r recursive
|
|
# -n line numbers
|
|
# -E extended regex (alternation)
|
|
# -I skip binary files
|
|
# --include / --exclude-dir for performance + scope
|
|
#
|
|
# We exclude *.md at the grep layer (docs may discuss the rule itself).
|
|
# We can't easily exclude infra/ or internal/ at the grep layer because
|
|
# they're not under the scan paths anyway — keeping the path-exclude
|
|
# regex below is defensive (if someone broadens SCAN_PATHS later the
|
|
# exclusions still apply).
|
|
RAW_HITS="$(
|
|
grep -rnEI \
|
|
--exclude='*.md' \
|
|
--exclude-dir='node_modules' \
|
|
--exclude-dir='.git' \
|
|
--exclude-dir='dist' \
|
|
--exclude-dir='build' \
|
|
--exclude-dir='.svelte-kit' \
|
|
"${JOINED_PATTERN}" \
|
|
"${EXISTING_PATHS[@]}" \
|
|
2>/dev/null \
|
|
|| true
|
|
)"
|
|
|
|
# Filter pass — drop excluded paths and excluded lines.
|
|
FILTERED_HITS=""
|
|
if [[ -n "${RAW_HITS}" ]]; then
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
# path is everything before the first ':<digits>:' line-number sep.
|
|
path="${line%%:*}"
|
|
if [[ "$path" =~ ${SCAN_EXCLUDES_REGEX} ]]; then
|
|
continue
|
|
fi
|
|
if [[ "$line" =~ ${LINE_EXCLUDES_REGEX} ]]; then
|
|
continue
|
|
fi
|
|
FILTERED_HITS+="${line}"$'\n'
|
|
done <<< "${RAW_HITS}"
|
|
fi
|
|
|
|
# Strip trailing newline introduced by the loop.
|
|
FILTERED_HITS="${FILTERED_HITS%$'\n'}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Report
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if [[ -z "${FILTERED_HITS}" ]]; then
|
|
_ok "no vendor-coupling violations found across ${#EXISTING_PATHS[@]} scan path(s)"
|
|
exit 0
|
|
fi
|
|
|
|
# We have violations. In hard-fail mode, fail; otherwise warn-only.
|
|
hit_count="$(echo "${FILTERED_HITS}" | wc -l | tr -d ' ')"
|
|
|
|
if [[ "${HARD_FAIL}" -eq 1 ]]; then
|
|
_err "${hit_count} vendor-coupling violation(s) found"
|
|
echo "" >&2
|
|
echo "${FILTERED_HITS}" >&2
|
|
echo "" >&2
|
|
echo "Vendor names must not appear in capability-named slots. The canonical-seam map" >&2
|
|
echo "is documented at docs/omantel-handover-wbs.md §3a; principle #4 in" >&2
|
|
echo "docs/INVIOLABLE-PRINCIPLES.md (\"never hardcode\") covers the rationale." >&2
|
|
echo "" >&2
|
|
echo "Common fixes:" >&2
|
|
echo " - rename '<vendor>-object-storage' Secret/Overlay to 'object-storage-credentials'" >&2
|
|
echo " - re-key chart values: 'veleroOverlay.hetzner.s3.x' → 'veleroOverlay.objectStorage.s3.x'" >&2
|
|
echo " - rename payload field '<vendor>ObjectStorage' → 'objectStorage' with provider tag" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Warn-only mode: emit and exit 0.
|
|
_warn "${hit_count} vendor-coupling reference(s) — WARN-ONLY (pre-#425 work-in-progress)"
|
|
echo "" >&2
|
|
echo "${FILTERED_HITS}" >&2
|
|
echo "" >&2
|
|
echo "These will become hard-fails once internal/objectstorage/ lands (PR #425)." >&2
|
|
echo "See docs/omantel-handover-wbs.md §3a + docs/INVIOLABLE-PRINCIPLES.md #4." >&2
|
|
exit 0
|