ci/woodpecker/push/publish Pipeline failed
Co-authored-by: code-be-02 <[email protected]>
199 lines
8.6 KiB
Bash
Executable File
199 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# repo-decl.sh — shared .mosaic/repo.json consumption for the git wrappers (T51 WP5b).
|
|
#
|
|
# Spec of record: docs/plans/2026-08-23_repo-structure-declaration.md (brain
|
|
# repo) sections 4 (consumption contract), 5.3 (normalization), 5.4
|
|
# (enforcement points), 1.2a (root anchoring). ALL consumers invoke the SAME
|
|
# WP1 validator (spec 5.1 — no in-process-only parsing of the declaration).
|
|
#
|
|
# Source this file, then call repo_decl_load once. It sets:
|
|
# DECL_STATE absent | invalid | valid
|
|
# DECL_FILE the declaration path that was inspected
|
|
# DECL_ERROR the validator's error line when DECL_STATE=invalid
|
|
# DECL_TRUNK / DECL_RELEASE / DECL_FLOW / DECL_REMOTE / DECL_POLICY /
|
|
# DECL_WT_ROOT / DECL_CLONE (populated only when DECL_STATE=valid)
|
|
# DECL_ORIGIN_N the normalized origin URL (when resolvable)
|
|
#
|
|
# Enforcement point 5.4(1): an invalid or unknown-version file counts as
|
|
# ABSENT for behavior, PLUS a loud error naming the file and the validator's
|
|
# key/reason — callers print DECL_ERROR (repo_decl_report_invalid) whenever
|
|
# they loaded something that failed validation; they do not silently ignore a
|
|
# broken file.
|
|
#
|
|
# Absence behavior (4.3) is the CALLER's policy (reversible vs irreversible;
|
|
# managed vs unmanaged — the adoption register is WP6, so during rollout every
|
|
# repo is unmanaged: warn + legacy). Helpers below provide the shared wordings.
|
|
#
|
|
# Root-dependent fields: consumers here read branch/flow/remote/policy only —
|
|
# NO path resolution happens in this library. The one helper that would
|
|
# resolve a host:/ path (repo_decl_path) fails closed while MOSAIC_HOST_ROOT
|
|
# is unset (1.2a: never guess a root), for any future caller that needs it.
|
|
#
|
|
# No output on success; diagnostics go to stderr.
|
|
|
|
REPO_DECL_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_DECL_VALIDATOR="$REPO_DECL_SCRIPT_DIR/../structure/validate-repo-json.sh"
|
|
|
|
repo_decl_warn() { printf 'repo-decl: %s\n' "$*" >&2; }
|
|
|
|
# Load and classify the declaration for the repo containing the current
|
|
# directory. Never fatal — classification is the product.
|
|
repo_decl_load() {
|
|
DECL_STATE=absent
|
|
DECL_FILE=""
|
|
DECL_ERROR=""
|
|
DECL_SCHEMA=""
|
|
DECL_TRUNK=""; DECL_RELEASE=""; DECL_FLOW=""; DECL_REMOTE=""
|
|
DECL_POLICY=""; DECL_WT_ROOT=""; DECL_CLONE=""
|
|
DECL_ORIGIN_N=""
|
|
|
|
local root
|
|
root="$(git rev-parse --show-toplevel 2>/dev/null)" || {
|
|
repo_decl_warn "no git repository — declaration consumption skipped"
|
|
return 0
|
|
}
|
|
DECL_FILE="$root/.mosaic/repo.json"
|
|
[ -f "$DECL_FILE" ] || return 0
|
|
|
|
if [ ! -x "$REPO_DECL_VALIDATOR" ] && [ ! -f "$REPO_DECL_VALIDATOR" ]; then
|
|
# 5.1 mandates the shared validator; a missing validator is an
|
|
# infrastructure failure, not an absent declaration.
|
|
repo_decl_warn "validator not found at $REPO_DECL_VALIDATOR — treating declaration as invalid"
|
|
DECL_STATE=invalid
|
|
DECL_ERROR="VALIDATION_ERROR validator: the shared validator is missing"
|
|
return 0
|
|
fi
|
|
|
|
local vout
|
|
if ! vout="$("$REPO_DECL_VALIDATOR" "$DECL_FILE" --mode display 2>&1)"; then
|
|
DECL_STATE=invalid
|
|
DECL_ERROR="$(printf '%s\n' "$vout" | grep -m1 'VALIDATION_ERROR' || printf '%s\n' "$vout" | head -1)"
|
|
return 0
|
|
fi
|
|
|
|
# Valid: extract the consumed fields via the same python stdlib the
|
|
# ecosystem already uses. Field-level grammar was the validator's job.
|
|
eval "$(python3 - "$DECL_FILE" <<'PY'
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
def q(k):
|
|
v = d.get(k, "")
|
|
return v if isinstance(v, str) else ""
|
|
sv = d.get("schema_version", 1)
|
|
print(f"DECL_SCHEMA={sv if isinstance(sv, int) and not isinstance(sv, bool) else 0!r}")
|
|
print(f"DECL_TRUNK={q('integration_trunk')!r}")
|
|
print(f"DECL_RELEASE={q('release_branch')!r}")
|
|
print(f"DECL_FLOW={q('flow')!r}")
|
|
print(f"DECL_REMOTE={q('canonical_remote')!r}")
|
|
print(f"DECL_POLICY={q('worktree_policy')!r}")
|
|
print(f"DECL_WT_ROOT={q('worktree_root')!r}")
|
|
print(f"DECL_CLONE={q('canonical_clone')!r}")
|
|
PY
|
|
)" || {
|
|
DECL_STATE=invalid
|
|
DECL_ERROR="VALIDATION_ERROR internal: field extraction failed"
|
|
return 0
|
|
}
|
|
DECL_STATE=valid
|
|
|
|
# 5.3: normalize origin once for remote comparisons (read callers warn,
|
|
# write callers refuse). An unresolvable origin is left empty — callers
|
|
# treat empty as "cannot compare" and act per their read/write policy.
|
|
local ourl
|
|
if ourl="$(git remote get-url origin 2>/dev/null)" && [ -n "$ourl" ]; then
|
|
DECL_ORIGIN_N="$("$REPO_DECL_VALIDATOR" --normalize-remote "$ourl" 2>/dev/null || true)"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# The mandatory loud error for an invalid file (5.4 point 1). Callers invoke
|
|
# this whenever DECL_STATE=invalid, regardless of their proceed/refuse policy.
|
|
repo_decl_report_invalid() {
|
|
repo_decl_warn "declaration INVALID at $DECL_FILE — $DECL_ERROR"
|
|
repo_decl_warn "treating the declaration as ABSENT (spec 5.4); legacy behavior follows"
|
|
}
|
|
|
|
# Shared absence wordings (4.3, rollout window: no adoption register yet, so
|
|
# every repo is unmanaged; warn + legacy per the Q-C ruling).
|
|
repo_decl_warn_absent_reversible() { # $1 = operation name
|
|
repo_decl_warn "no .mosaic/repo.json — $1 is unmanaged during rollout: legacy behavior, no declaration guarantees (spec 4.3)"
|
|
}
|
|
repo_decl_warn_absent_irreversible() { # $1 = operation name
|
|
repo_decl_warn "no .mosaic/repo.json — $1 proceeds on LEGACY assumptions during the rollout window; declaration-validated transitions unavailable (spec 4.3)"
|
|
}
|
|
|
|
# Remote comparison (5.3). rc 0 match/unknown, rc 1 mismatch.
|
|
repo_decl_remote_matches() {
|
|
[ "$DECL_STATE" = valid ] || return 0
|
|
[ -n "$DECL_ORIGIN_N" ] && [ -n "$DECL_REMOTE" ] || return 0
|
|
local want
|
|
want="$("$REPO_DECL_VALIDATOR" --normalize-remote "$DECL_REMOTE" 2>/dev/null || true)"
|
|
[ -n "$want" ] || return 0
|
|
[ "$DECL_ORIGIN_N" = "$want" ]
|
|
}
|
|
|
|
# Resolve a host:/-anchored declaration path (1.2a). Fails CLOSED while
|
|
# MOSAIC_HOST_ROOT is unset or empty — never guesses a root. No WP5b consumer
|
|
# calls this today; it exists so the first one that needs a path cannot
|
|
# silently guess.
|
|
repo_decl_path() { # $1 = host:/... value; prints the resolved absolute path
|
|
local v="${1:-}" root="${MOSAIC_HOST_ROOT:-}"
|
|
case "$v" in
|
|
host:/*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
if [ -z "$root" ]; then
|
|
repo_decl_warn "MOSAIC_HOST_ROOT is unset — refusing to resolve '$v' (spec 1.2a fail-closed; never guess a root)"
|
|
return 1
|
|
fi
|
|
printf '%s/%s\n' "${root%/}" "${v#host:/}"
|
|
}
|
|
|
|
# Transition validation (4.2: a CLI flag is input, not authority).
|
|
# rc 0 = allowed; rc 1 = forbidden (message on stderr); rc 2 = no valid
|
|
# declaration (caller applies its absence policy).
|
|
# flow=direct: base must be the trunk (trunk == release); head must
|
|
# differ from it.
|
|
# flow=trunk-release: feature->trunk allowed; trunk->release allowed (release
|
|
# promotion: head IS the trunk); anything else refused —
|
|
# feature->release explicitly REJECTED.
|
|
repo_decl_check_transition() { # $1 head, $2 base
|
|
[ "$DECL_STATE" = valid ] || return 2
|
|
local head="$1" base="$2"
|
|
if [ -z "$head" ] || [ -z "$base" ]; then
|
|
repo_decl_warn "transition check needs a head and a base (got head='$head' base='$base')"
|
|
return 1
|
|
fi
|
|
if [ "$head" = "$base" ]; then
|
|
repo_decl_warn "forbidden transition: head '$head' equals base '$base'"
|
|
return 1
|
|
fi
|
|
case "$DECL_FLOW" in
|
|
direct)
|
|
if [ "$base" = "$DECL_TRUNK" ]; then
|
|
return 0
|
|
fi
|
|
repo_decl_warn "forbidden transition (flow=direct): base must be the trunk '$DECL_TRUNK', got '$base'"
|
|
return 1
|
|
;;
|
|
trunk-release)
|
|
if [ "$base" = "$DECL_TRUNK" ] && [ "$head" != "$DECL_TRUNK" ] && [ "$head" != "$DECL_RELEASE" ]; then
|
|
return 0 # feature -> trunk
|
|
fi
|
|
if [ "$head" = "$DECL_TRUNK" ] && [ "$base" = "$DECL_RELEASE" ]; then
|
|
return 0 # release promotion: trunk -> release
|
|
fi
|
|
if [ "$base" = "$DECL_RELEASE" ] && [ "$head" != "$DECL_TRUNK" ]; then
|
|
repo_decl_warn "forbidden transition (flow=trunk-release): feature->release is REJECTED (head '$head' -> release '$DECL_RELEASE'); promote via $DECL_TRUNK"
|
|
return 1
|
|
fi
|
|
repo_decl_warn "forbidden transition (flow=trunk-release): '$head' -> '$base' is not a declared transition (feature->$DECL_TRUNK or $DECL_TRUNK->$DECL_RELEASE)"
|
|
return 1
|
|
;;
|
|
*)
|
|
repo_decl_warn "unknown declared flow '$DECL_FLOW'"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|