muse-bridge-deploy.sh bash
227 lines 9.8 KB
Raw
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