muse-bridge-deploy.sh
bash
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠ breaking
10 days ago
| 1 | #!/usr/bin/env bash |
| 2 | # muse-bridge-deploy.sh |
| 3 | # |
| 4 | # Complete MuseHub → external Git deployment pipeline. |
| 5 | # |
| 6 | # Usage: |
| 7 | # ./scripts/muse-bridge-deploy.sh "mirror: brief description of what changed" |
| 8 | # |
| 9 | # What this does (in order): |
| 10 | # 1. Security audit — npm audit fix (blocks the bridge if high vulns remain) |
| 11 | # 2. Commit audit fix — if audit changed package-lock.json, commits it to Muse |
| 12 | # 3. Bridge export — muse bridge git-export → isolated .muse/mirror checkout |
| 13 | # 4. GitHub PR — opens a PR from muse-mirror to main (skips if one already exists) |
| 14 | # |
| 15 | # Requirements: |
| 16 | # - muse CLI in PATH (from ~/.local/share/muse/venv/bin) |
| 17 | # - gh CLI in PATH and authenticated (gh auth status) |
| 18 | # - git remote "origin" points to your GitHub/GitLab repo |
| 19 | # - MUSE_BRIDGE_GIT_BRANCH env var (default: muse-mirror) |
| 20 | # - MUSE_BRIDGE_BASE_BRANCH env var (default: main) |
| 21 | # - MUSE_BRIDGE_MIRROR_DIR env var (default: .muse/mirror) |
| 22 | |
| 23 | set -euo pipefail |
| 24 | |
| 25 | # ── Config (override via env) ────────────────────────────────────────────────── |
| 26 | MIRROR_BRANCH="${MUSE_BRIDGE_GIT_BRANCH:-muse-mirror}" |
| 27 | BASE_BRANCH="${MUSE_BRIDGE_BASE_BRANCH:-main}" |
| 28 | PR_TITLE="${1:-mirror: deploy from MuseHub $(date '+%Y-%m-%d')}" |
| 29 | REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" |
| 30 | MIRROR_DIR="${MUSE_BRIDGE_MIRROR_DIR:-.muse/mirror}" |
| 31 | |
| 32 | # ── Colors ───────────────────────────────────────────────────────────────────── |
| 33 | RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; NC='\033[0m' |
| 34 | info() { echo -e "${BLUE}[bridge]${NC} $*"; } |
| 35 | success() { echo -e "${GREEN}[bridge]${NC} $*"; } |
| 36 | warn() { echo -e "${YELLOW}[bridge]${NC} $*"; } |
| 37 | fail() { echo -e "${RED}[bridge] ERROR:${NC} $*" >&2; exit 1; } |
| 38 | |
| 39 | cd "$REPO_ROOT" |
| 40 | |
| 41 | if [[ "$MIRROR_DIR" = /* ]]; then |
| 42 | MIRROR_DIR_ABS="$MIRROR_DIR" |
| 43 | else |
| 44 | MIRROR_DIR_ABS="${REPO_ROOT}/${MIRROR_DIR}" |
| 45 | fi |
| 46 | |
| 47 | if [[ "$MIRROR_DIR_ABS" == "$REPO_ROOT" ]]; then |
| 48 | fail "MUSE_BRIDGE_MIRROR_DIR must not be the repository root. Refusing unsafe --git-dir . workflow." |
| 49 | fi |
| 50 | |
| 51 | cleanup_sentinels() { |
| 52 | [[ -n "${ENV_SENTINEL:-}" && -f "$ENV_SENTINEL" ]] && rm -f "$ENV_SENTINEL" |
| 53 | [[ -n "${CONFIG_SENTINEL:-}" && -f "$CONFIG_SENTINEL" ]] && rm -f "$CONFIG_SENTINEL" |
| 54 | [[ -n "${DATA_SENTINEL:-}" && -f "$DATA_SENTINEL" ]] && rm -f "$DATA_SENTINEL" |
| 55 | [[ "${CREATED_DATA_DIR:-0}" == "1" && -d "${REPO_ROOT}/data" ]] && rmdir "${REPO_ROOT}/data" 2>/dev/null || true |
| 56 | } |
| 57 | trap cleanup_sentinels EXIT |
| 58 | |
| 59 | ensure_mirror_checkout() { |
| 60 | local remote_url |
| 61 | remote_url=$(git config --get remote.origin.url 2>/dev/null || true) |
| 62 | [[ -n "$remote_url" ]] || fail "Git remote 'origin' is not configured." |
| 63 | |
| 64 | if [[ -e "$MIRROR_DIR_ABS" && ! -d "${MIRROR_DIR_ABS}/.git" ]]; then |
| 65 | fail "Mirror path exists but is not a git repository: ${MIRROR_DIR_ABS}" |
| 66 | fi |
| 67 | |
| 68 | if [[ ! -d "${MIRROR_DIR_ABS}/.git" ]]; then |
| 69 | info "Provisioning isolated mirror checkout at ${MIRROR_DIR}..." |
| 70 | mkdir -p "$(dirname "$MIRROR_DIR_ABS")" |
| 71 | git clone --single-branch --branch "$MIRROR_BRANCH" "$remote_url" "$MIRROR_DIR_ABS" |
| 72 | fi |
| 73 | |
| 74 | git -C "$MIRROR_DIR_ABS" remote set-url origin "$remote_url" |
| 75 | git -C "$MIRROR_DIR_ABS" fetch origin "$MIRROR_BRANCH" |
| 76 | git -C "$MIRROR_DIR_ABS" checkout -B "$MIRROR_BRANCH" "origin/${MIRROR_BRANCH}" |
| 77 | git -C "$MIRROR_DIR_ABS" reset --hard "origin/${MIRROR_BRANCH}" |
| 78 | } |
| 79 | |
| 80 | prepare_sentinels() { |
| 81 | ENV_SENTINEL="${REPO_ROOT}/.env.bridge-sentinel.$$" |
| 82 | CONFIG_SENTINEL="${REPO_ROOT}/config/bridge-sentinel-local.$$.yaml" |
| 83 | CREATED_DATA_DIR=0 |
| 84 | |
| 85 | if [[ ! -d "${REPO_ROOT}/data" ]]; then |
| 86 | mkdir -p "${REPO_ROOT}/data" |
| 87 | CREATED_DATA_DIR=1 |
| 88 | fi |
| 89 | DATA_SENTINEL="${REPO_ROOT}/data/bridge-sentinel.$$.db" |
| 90 | |
| 91 | printf 'bridge sentinel: no secrets\n' > "$ENV_SENTINEL" |
| 92 | printf 'bridge sentinel: no secrets\n' > "$CONFIG_SENTINEL" |
| 93 | printf 'bridge sentinel: no secrets\n' > "$DATA_SENTINEL" |
| 94 | } |
| 95 | |
| 96 | verify_sentinels() { |
| 97 | [[ -f "$ENV_SENTINEL" ]] || fail "Bridge safety sentinel disappeared: ${ENV_SENTINEL}. The bridge touched the dev tree." |
| 98 | [[ -f "$CONFIG_SENTINEL" ]] || fail "Bridge safety sentinel disappeared: ${CONFIG_SENTINEL}. The bridge touched the dev tree." |
| 99 | [[ -f "$DATA_SENTINEL" ]] || fail "Bridge safety sentinel disappeared: ${DATA_SENTINEL}. The bridge touched the dev tree." |
| 100 | } |
| 101 | |
| 102 | # ── Step 1: Security audit ───────────────────────────────────────────────────── |
| 103 | info "Step 1/4 — Running security audit (npm audit fix)..." |
| 104 | |
| 105 | if [[ ! -f package.json ]]; then |
| 106 | warn "No package.json found — skipping npm audit. Add audits for your stack manually." |
| 107 | else |
| 108 | AUDIT_FIX_OUTPUT=$(npm audit fix --audit-level=moderate 2>&1 || true) |
| 109 | printf '%s\n' "$AUDIT_FIX_OUTPUT" | tail -5 |
| 110 | |
| 111 | # Check if high/critical vulns remain after auto-fix. |
| 112 | # Capture audit JSON into a variable FIRST. `npm audit --json` exits non-zero when |
| 113 | # vulnerabilities exist, and with `set -o pipefail` a `cmd | python3 || echo 0` chain |
| 114 | # would append "0" to python's output (yielding a multi-line "N\n0" that breaks the |
| 115 | # numeric `[[ -gt ]]` test). Separating capture from parse and normalizing to a single |
| 116 | # integer makes this robust regardless of audit exit code / output shape. |
| 117 | AUDIT_JSON=$(npm audit --json 2>/dev/null || true) |
| 118 | HIGH_COUNT=$(printf '%s' "$AUDIT_JSON" | python3 -c " |
| 119 | import json, sys |
| 120 | try: |
| 121 | d = json.load(sys.stdin) |
| 122 | v = d.get('metadata', {}).get('vulnerabilities', {}) |
| 123 | print(int(v.get('high', 0)) + int(v.get('critical', 0))) |
| 124 | except Exception: |
| 125 | print(0) |
| 126 | " 2>/dev/null || echo "0") |
| 127 | HIGH_COUNT=$(printf '%s\n' "$HIGH_COUNT" | tail -1 | tr -cd '0-9'); HIGH_COUNT=${HIGH_COUNT:-0} |
| 128 | |
| 129 | if (( HIGH_COUNT > 0 )); then |
| 130 | fail "npm audit found $HIGH_COUNT high/critical vulnerabilities that could not be auto-fixed.\n\ |
| 131 | Run 'npm audit' to review them, fix manually, then re-run this script.\n\ |
| 132 | DO NOT bridge with known high/critical vulnerabilities." |
| 133 | fi |
| 134 | success "Audit clean — 0 high/critical vulnerabilities." |
| 135 | fi |
| 136 | |
| 137 | # ── Step 2: Commit audit changes if any ─────────────────────────────────────── |
| 138 | info "Step 2/4 — Checking for audit-generated changes..." |
| 139 | |
| 140 | AUDIT_CHANGED=$(muse status --short 2>/dev/null | grep -E "package-lock\.json|package\.json" || true) |
| 141 | |
| 142 | if [[ -n "$AUDIT_CHANGED" ]]; then |
| 143 | warn "Audit changed package files — committing to Muse before bridging." |
| 144 | muse code add package-lock.json package.json 2>/dev/null || true |
| 145 | muse commit -m "security: npm audit fix pre-bridge $(date '+%Y-%m-%d')" |
| 146 | muse push staging "${BASE_BRANCH}" |
| 147 | success "Audit commit pushed to Muse main." |
| 148 | else |
| 149 | success "No audit changes to commit." |
| 150 | fi |
| 151 | |
| 152 | # ── Step 3: Bridge export ────────────────────────────────────────────────────── |
| 153 | info "Step 3/4 — Bridging Muse main → ${MIRROR_BRANCH} via isolated mirror checkout..." |
| 154 | |
| 155 | ensure_mirror_checkout |
| 156 | prepare_sentinels |
| 157 | |
| 158 | muse bridge git-export \ |
| 159 | --git-dir "${MIRROR_DIR_ABS}" \ |
| 160 | --git-branch "${MIRROR_BRANCH}" \ |
| 161 | --git-remote origin \ |
| 162 | --force-push \ |
| 163 | --strip-muse-metadata \ |
| 164 | --exclude '.muse/*' \ |
| 165 | --exclude '.muse/**' \ |
| 166 | --exclude '.env' \ |
| 167 | --exclude '.env.local' \ |
| 168 | --exclude '.env.*.local' \ |
| 169 | --exclude 'config/local.yaml' \ |
| 170 | --exclude 'config/*-local.*' \ |
| 171 | --exclude 'data/*' \ |
| 172 | --exclude 'data/**' \ |
| 173 | --exclude '*.db' \ |
| 174 | --exclude '*.sqlite' |
| 175 | |
| 176 | verify_sentinels |
| 177 | |
| 178 | # `muse bridge git-export --exclude` prevents ADDING .muse/, but it does NOT prune |
| 179 | # .muse/ files already tracked in the mirror branch from before the exclude existed. |
| 180 | # Purge them explicitly so the audit mirror never carries Muse-internal metadata. |
| 181 | # Idempotent: a no-op once the mirror is clean (Knowtation's mirror is already clean). |
| 182 | MIRROR_TRACKED_MUSE=$(git -C "${MIRROR_DIR_ABS}" ls-files .muse) |
| 183 | if [[ -n "$MIRROR_TRACKED_MUSE" ]]; then |
| 184 | info "Purging pre-existing tracked .muse/ metadata from ${MIRROR_BRANCH}..." |
| 185 | git -C "${MIRROR_DIR_ABS}" rm -r --quiet .muse |
| 186 | git -C "${MIRROR_DIR_ABS}" commit -q -m "chore(bridge): purge Muse-internal .muse/ metadata from mirror" |
| 187 | git -C "${MIRROR_DIR_ABS}" push origin "HEAD:${MIRROR_BRANCH}" |
| 188 | success "Mirror .muse/ metadata purged and pushed." |
| 189 | fi |
| 190 | |
| 191 | success "Bridge complete. ${MIRROR_BRANCH} is up to date on origin." |
| 192 | |
| 193 | # ── Step 4: GitHub/GitLab PR ────────────────────────────────────────────────── |
| 194 | info "Step 4/4 — Opening or updating PR: ${MIRROR_BRANCH} → ${BASE_BRANCH}..." |
| 195 | |
| 196 | if ! command -v gh &>/dev/null; then |
| 197 | warn "gh CLI not found — skipping PR creation." |
| 198 | warn "Manually open a PR from '${MIRROR_BRANCH}' to '${BASE_BRANCH}' on your Git host." |
| 199 | exit 0 |
| 200 | fi |
| 201 | |
| 202 | # Check if a PR already exists for this branch |
| 203 | EXISTING_PR=$(gh pr list --head "${MIRROR_BRANCH}" --base "${BASE_BRANCH}" --json number,url \ |
| 204 | --jq '.[0].url' 2>/dev/null || true) |
| 205 | |
| 206 | if [[ -n "$EXISTING_PR" ]]; then |
| 207 | warn "PR already exists: $EXISTING_PR" |
| 208 | warn "Review and merge it at the link above. (The bridge already updated the branch.)" |
| 209 | else |
| 210 | PR_URL=$(gh pr create \ |
| 211 | --base "${BASE_BRANCH}" \ |
| 212 | --head "${MIRROR_BRANCH}" \ |
| 213 | --title "${PR_TITLE}" \ |
| 214 | --body "Automated mirror from MuseHub. All development and review happened there. |
| 215 | |
| 216 | - Security audit passed (0 high/critical vulnerabilities) |
| 217 | - Bridged via: \`muse bridge git-export\` |
| 218 | - Source: \`muse status\` on Muse main at time of bridge |
| 219 | |
| 220 | Merge this PR to trigger your deployment platform." \ |
| 221 | 2>&1) |
| 222 | success "PR created: $PR_URL" |
| 223 | fi |
| 224 | |
| 225 | echo "" |
| 226 | success "Deploy pipeline complete." |
| 227 | echo -e " Next step: merge the PR on GitHub/GitLab and your deployment platform will pick it up." |
File History
3 commits
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠
10 days ago
sha256:d8c648b20a4d53b2673c5c082ee7edfa7b2fc9b11080832da1f38807b6bf940b
fix(7C-L1b): route hosted delegation proposals through cani…
Human
minor
⚠
30 days ago
sha256:2827ba9e7632a4b141c50caf1e8f7d77abbc3515be20e7465f2bccb0ac4edf91
fix: repair endpoint now sets has_active_subscription when …
Human
minor
⚠
49 days ago