#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' usage: scripts/promote-stable-runtime [--root ] [--promoted ] \ [--source-branch ] [--source-pr ] \ [--restart-method ] [--rollback ] \ [--health-check-proof ] \ [--identity-proof ] [--profile-proof ] \ [--workspace-proof ] \ [--mutation-capability-proof ] Emit and validate a stable-control-runtime promotion record (#615). This helper is READ-ONLY. It never fetches, merges, restarts, reloads, or kills anything: promotion itself is an operator action documented in docs/stable-runtime-promotion-runbook.md. The helper reads the current runtime state, assembles the required record, validates it with stable_control_runtime.assess_promotion_record(), and prints it for the operator to act on and archive. Defaults: --root the repository root containing this script --promoted HEAD of that root Exit status is non-zero when the assembled record is incomplete, so a promotion cannot be recorded without its proof fields. Example: scripts/promote-stable-runtime \ --source-branch feat/issue-615-runtime-mode-enforcement \ --source-pr 770 \ --restart-method "IDE client reconnect (/mcp)" \ --rollback "git -C merge --ff-only ; reconnect client" \ --health-check-proof "gitea_assess_mcp_namespace_health: all four healthy" \ --identity-proof "gitea_whoami per namespace" \ --profile-proof "gitea_get_runtime_context per namespace" \ --workspace-proof "process root == canonical root; clean" \ --mutation-capability-proof "gitea_resolve_task_capability: allowed" EOF } ROOT="" PROMOTED="" SOURCE_BRANCH="" SOURCE_PR="" RESTART_METHOD="" ROLLBACK="" HEALTH_PROOF="" IDENTITY_PROOF="" PROFILE_PROOF="" WORKSPACE_PROOF="" CAPABILITY_PROOF="" while [[ $# -gt 0 ]]; do case "$1" in --root) ROOT="${2:-}"; shift 2 ;; --promoted) PROMOTED="${2:-}"; shift 2 ;; --source-branch) SOURCE_BRANCH="${2:-}"; shift 2 ;; --source-pr) SOURCE_PR="${2:-}"; shift 2 ;; --restart-method) RESTART_METHOD="${2:-}"; shift 2 ;; --rollback) ROLLBACK="${2:-}"; shift 2 ;; --health-check-proof) HEALTH_PROOF="${2:-}"; shift 2 ;; --identity-proof) IDENTITY_PROOF="${2:-}"; shift 2 ;; --profile-proof) PROFILE_PROOF="${2:-}"; shift 2 ;; --workspace-proof) WORKSPACE_PROOF="${2:-}"; shift 2 ;; --mutation-capability-proof) CAPABILITY_PROOF="${2:-}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage; exit 2 ;; esac done SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="${ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" if ! git -C "$ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then echo "error: '$ROOT' is not a git checkout; cannot read runtime SHAs" >&2 exit 1 fi PREVIOUS="${GITEA_MCP_PREVIOUS_RUNTIME_SHA:-}" if [[ -z "$PREVIOUS" ]]; then # The runtime the operator is replacing. Best-effort: the commit master # pointed at before the fast-forward, recorded in the reflog. PREVIOUS="$(git -C "$ROOT" rev-parse 'master@{1}' 2>/dev/null || true)" fi PROMOTED="${PROMOTED:-$(git -C "$ROOT" rev-parse HEAD)}" BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)" DIRTY="$(git -C "$ROOT" status --porcelain | wc -l | tr -d ' ')" STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" if [[ -z "$WORKSPACE_PROOF" ]]; then WORKSPACE_PROOF="root=$ROOT branch=$BRANCH dirty_files=$DIRTY" fi if [[ -z "$ROLLBACK" && -n "$PREVIOUS" ]]; then ROLLBACK="git -C $ROOT merge --ff-only $PREVIOUS (or checkout $PREVIOUS), then reload the runtime by the same sanctioned method and re-prove every namespace" fi cat <} promoted_runtime_sha: ${PROMOTED} source_branch: ${SOURCE_BRANCH:-} source_pr: ${SOURCE_PR:-} restart_method: ${RESTART_METHOD:-} health_check_proof: ${HEALTH_PROOF:-} identity_proof: ${IDENTITY_PROOF:-} profile_proof: ${PROFILE_PROOF:-} workspace_proof: ${WORKSPACE_PROOF} mutation_capability_proof: ${CAPABILITY_PROOF:-} rollback_instructions: ${ROLLBACK:-} EOF if [[ "$DIRTY" != "0" ]]; then { echo echo "WARNING: the stable checkout has $DIRTY dirty file(s); a dirty stable" echo " runtime is itself a mutation blocker (dirty_stable_runtime_checkout)." } >&2 fi PYTHON_BIN="${PYTHON_BIN:-python3}" RECORD_JSON="$( ROOT="$ROOT" \ PREVIOUS="$PREVIOUS" PROMOTED="$PROMOTED" \ SOURCE_BRANCH="$SOURCE_BRANCH" SOURCE_PR="$SOURCE_PR" \ RESTART_METHOD="$RESTART_METHOD" HEALTH_PROOF="$HEALTH_PROOF" \ IDENTITY_PROOF="$IDENTITY_PROOF" PROFILE_PROOF="$PROFILE_PROOF" \ WORKSPACE_PROOF="$WORKSPACE_PROOF" CAPABILITY_PROOF="$CAPABILITY_PROOF" \ ROLLBACK="$ROLLBACK" \ "$PYTHON_BIN" -c ' import json import os print(json.dumps({ "previous_runtime_sha": os.environ.get("PREVIOUS", ""), "promoted_runtime_sha": os.environ.get("PROMOTED", ""), "source_branch": os.environ.get("SOURCE_BRANCH", ""), "source_pr": os.environ.get("SOURCE_PR", ""), "restart_method": os.environ.get("RESTART_METHOD", ""), "health_check_proof": os.environ.get("HEALTH_PROOF", ""), "identity_proof": os.environ.get("IDENTITY_PROOF", ""), "profile_proof": os.environ.get("PROFILE_PROOF", ""), "workspace_proof": os.environ.get("WORKSPACE_PROOF", ""), "mutation_capability_proof": os.environ.get("CAPABILITY_PROOF", ""), "rollback_instructions": os.environ.get("ROLLBACK", ""), })) ' )" RECORD_JSON="$RECORD_JSON" ROOT="$ROOT" "$PYTHON_BIN" -c ' import json import os import sys sys.path.insert(0, os.environ["ROOT"]) import stable_control_runtime as scr record = json.loads(os.environ["RECORD_JSON"]) assessment = scr.assess_promotion_record(record) print() print("validation:", json.dumps(assessment, indent=2)) print() if not assessment["valid"]: print("Promotion record is INCOMPLETE - do not archive it as a promotion.") sys.exit(1) print("Promotion record is complete. Archive it on the tracking issue.") '