#!/usr/bin/env bash # # governance-check.sh — local/CI parity governance gate for the Seahaven backend. # # Locally this runs G1–G5, G10, G11, promotion-script tests, and live G13. # The architecture job in ci.yml sets GOVERNANCE_SKIP_BUILD_TEST=1 so G4/G5 # run only in the Build and test job. Live G13 runs on pull_request and local # invocations; it skips merge_group and push. # # Usage: # bash scripts/governance-check.sh # BASE_REF=origin/main bash scripts/governance-check.sh # BASE_REF= HEAD_REF= bash scripts/governance-check.sh set -euo pipefail SOLUTION="SeaHavenIndustries.sln" ARCH_TEST_PROJECT="Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj" LIVE_ROOTS=(terraform/live/dev terraform/live/staging) log() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; } ok() { printf '\033[32mPASS\033[0m %s\n' "$1"; } bad() { printf '\033[31mFAIL\033[0m %s\n' "$1"; } if [[ -n "${DOTNET_BIN:-}" ]]; then DOTNET="$DOTNET_BIN" elif command -v dotnet >/dev/null 2>&1; then DOTNET="$(command -v dotnet)" elif [[ -x "$HOME/.dotnet/dotnet" ]]; then DOTNET="$HOME/.dotnet/dotnet" else bad "dotnet is unavailable; set DOTNET_BIN or install the repository SDK." exit 1 fi # Comparison point for changed-file formatting. Default to main locally; CI # overrides BASE_REF/HEAD_REF with the PR base/head SHAs. BASE_REF="${BASE_REF:-origin/main}" HEAD_REF="${HEAD_REF:-HEAD}" # Resolve the base ref before using it for a diff. if ! git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null; then bad "G3: BASE_REF '${BASE_REF}' does not resolve to a commit (run: git fetch origin)." exit 1 fi # Diff the change set from the merge base, not from the base tip. A two-dot # diff against a moving base reports everything the base gained after the # branch point as if this change reverted it, so a branch behind main that # touches C# would fail G13 whenever main had merged Terraform in the meantime. # The merge queue no longer requires branches to be current, so this matters. DIFF_BASE="$(git merge-base "${BASE_REF}" "${HEAD_REF}")" || { bad "G3: no merge base between '${BASE_REF}' and '${HEAD_REF}'." exit 1 } log "G1: restore" "$DOTNET" restore "$SOLUTION" ok "G1: restore" log "G2: architecture boundary tests (dependency direction)" "$DOTNET" test "$ARCH_TEST_PROJECT" \ --no-restore \ --filter "FullyQualifiedName~ArchitectureTests" \ --nologo ok "G2: ArchitectureTests" log "G3: changed-file formatting (${BASE_REF}...${HEAD_REF}, merge base ${DIFF_BASE:0:7})" changed_cs=() while IFS= read -r f; do changed_cs+=("$f") done < <( git diff --name-only --diff-filter=ACMR "${DIFF_BASE}" "${HEAD_REF}" -- '*.cs' ) if (( ${#changed_cs[@]} == 0 )); then printf '\033[33mSKIP\033[0m G3: no changed C# files between %s...%s\n' "${BASE_REF}" "${HEAD_REF}" else printf ' checking %d changed C# file(s)\n' "${#changed_cs[@]}" "$DOTNET" format "$SOLUTION" \ --no-restore \ --verify-no-changes \ --include "${changed_cs[@]}" ok "G3: changed-file formatting" fi if [[ "${GOVERNANCE_SKIP_BUILD_TEST:-}" == "1" ]]; then printf '\033[33mSKIP\033[0m G4: Release build (CI Build and test job owns it)\n' printf '\033[33mSKIP\033[0m G5: full test suite (CI Build and test job owns it)\n' else log "G4: Release build" "$DOTNET" build "$SOLUTION" -c Release --no-restore --nologo ok "G4: Release build" log "G5: full test suite" "$DOTNET" test "$SOLUTION" -c Release --no-build --nologo ok "G5: full test suite" fi log "G10: Terraform import plan safety" python scripts/test-terraform-import-plan-check.py ok "G10: Terraform import plan safety" log "Release promotion scripts" python3 scripts/test_next_release_tag.py python3 scripts/test_require_commit_checks.py python3 scripts/test_check_app_terraform_isolation.py ok "Release promotion scripts" log "G11: Terraform formatting and validation" if ! command -v terraform >/dev/null 2>&1; then bad "G11: terraform is unavailable; install Terraform or set PATH." exit 1 fi terraform fmt -check -recursive terraform for live_root in "${LIVE_ROOTS[@]}"; do terraform -chdir="${live_root}" init -backend=false -input=false -lockfile=readonly -no-color terraform -chdir="${live_root}" validate -no-color done ok "G11: Terraform formatting and validation" if [[ -n "${GITHUB_EVENT_NAME:-}" && "${GITHUB_EVENT_NAME}" != "pull_request" ]]; then printf '\033[33mSKIP\033[0m G13: live isolation is a pull-request property (event: %s)\n' "${GITHUB_EVENT_NAME}" else log "G13: application and Terraform isolation (${BASE_REF}...${HEAD_REF}, merge base ${DIFF_BASE:0:7})" python3 scripts/check_app_terraform_isolation.py < <( git diff --name-only --diff-filter=ACMR "${DIFF_BASE}" "${HEAD_REF}" ) ok "G13: application and Terraform isolation" fi log "governance-check: all required repository gates passed"