From 82eed086838e8428fb785406f8d39a58b9d93bea Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Wed, 26 Aug 2026 12:07:32 -0500 Subject: [PATCH] Add Galaxy build and deployment workflows --- AGENTS.md | 4 + Makefile | 13 +++ script/build-and-deploy-hermes/src/index.tsx | 99 ++++++++++++------- .../src/index.tsx | 66 +++++++++---- script/build-and-upload-hermes.sh | 7 ++ script/install-galaxy.sh | 17 +--- 6 files changed, 141 insertions(+), 65 deletions(-) create mode 100644 Makefile create mode 100755 script/build-and-upload-hermes.sh diff --git a/AGENTS.md b/AGENTS.md index 0415ad26..234d7210 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,10 @@ This file provides guidance when working with code in this repository. ### Build and Run - `cargo run` - Build and run Warp locally - `cargo bundle --bin warp` - Bundle the main app +- `make run` - Build and run `galaxy-oss` +- `make clean` - Remove Cargo build artifacts +- `make deploy` - Validate, update, clean, bundle, and upload Galaxy to Hermes +- `make install` - Validate, update, clean, bundle, and install Galaxy to `/Applications` ### Running with local warp-server To connect Warp client to a local warp-server instance: diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..478edebe --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: run clean deploy install + +run: + cargo run --bin galaxy-oss + +clean: + cargo clean + +deploy: + ./script/build-and-upload-hermes.sh + +install: + ./script/build-and-install-to-applications.sh diff --git a/script/build-and-deploy-hermes/src/index.tsx b/script/build-and-deploy-hermes/src/index.tsx index e9f9e196..9852b529 100644 --- a/script/build-and-deploy-hermes/src/index.tsx +++ b/script/build-and-deploy-hermes/src/index.tsx @@ -267,7 +267,10 @@ function StepLine({ step }: { step: Step }) { function App() { const [steps, setSteps] = useState([ - { label: "Build Galaxy", status: "pending" }, + { label: "Check working directory", status: "pending" }, + { label: "Pull current branch", status: "pending" }, + { label: "Clean Cargo build artifacts", status: "pending" }, + { label: "Bundle Galaxy", status: "pending" }, { label: "Create Galaxy.zip", status: "pending" }, { label: "Authenticate with Hermes", status: "pending" }, { label: "Start multipart upload", status: "pending" }, @@ -293,17 +296,45 @@ function App() { useEffect(() => { (async () => { try { - // ─── Step 0: Build ────────────────────────────────────────────── + // ─── Step 0: Check working directory ─────────────────────────── updateStep(0, { status: "running" }); await runCommandStreaming( - "cargo bundle --bin galaxy-oss --package galaxy", + `if [ -n "$(git status --porcelain)" ]; then git status --short; exit 1; fi`, WORKSPACE_ROOT, (line) => appendLog(0, line) ); updateStep(0, { status: "done" }); - // ─── Step 1: Zip ──────────────────────────────────────────────── + // ─── Step 1: Pull current branch ──────────────────────────────── updateStep(1, { status: "running" }); + const branch = execSync("git branch --show-current", { + cwd: WORKSPACE_ROOT, + encoding: "utf8", + }).trim(); + if (!branch) throw new Error("Cannot deploy from a detached HEAD"); + await runCommandStreaming( + `git pull --ff-only origin "${branch}"`, + WORKSPACE_ROOT, + (line) => appendLog(1, line) + ); + updateStep(1, { status: "done", detail: branch }); + + // ─── Step 2: Clean ────────────────────────────────────────────── + updateStep(2, { status: "running" }); + await runCommandStreaming("cargo clean", WORKSPACE_ROOT, (line) => appendLog(2, line)); + updateStep(2, { status: "done" }); + + // ─── Step 3: Bundle ───────────────────────────────────────────── + updateStep(3, { status: "running" }); + await runCommandStreaming( + "cargo bundle --bin galaxy-oss --package galaxy", + WORKSPACE_ROOT, + (line) => appendLog(3, line) + ); + updateStep(3, { status: "done" }); + + // ─── Step 4: Zip ──────────────────────────────────────────────── + updateStep(4, { status: "running" }); // Find the .app bundle — cargo bundle outputs to target/debug/bundle/osx/ const appDir = path.join( @@ -314,19 +345,19 @@ function App() { // Remove old zip if exists execSync(`rm -f "${zipPath}"`, { stdio: "pipe" }); - // Zip the .app folder + // Preserve executable permissions, symlinks, and macOS bundle metadata. await runCommandStreaming( - `cd "${appDir}" && zip -r -y "${zipPath}" Galaxy.app`, + `ditto -c -k --keepParent "${path.join(appDir, "Galaxy.app")}" "${zipPath}"`, WORKSPACE_ROOT, - (line) => appendLog(1, line) + (line) => appendLog(4, line) ); const fileSize = statSync(zipPath).size; const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(1); - updateStep(1, { status: "done", detail: `${fileSizeMB} MB` }); + updateStep(4, { status: "done", detail: `${fileSizeMB} MB` }); - // ─── Step 2: Authenticate ─────────────────────────────────────── - updateStep(2, { status: "running" }); + // ─── Step 5: Authenticate ─────────────────────────────────────── + updateStep(5, { status: "running" }); const loginRes = await apiPost<{ token: string }>( `${BASE_URL}/api/auth/login`, @@ -334,11 +365,11 @@ function App() { COMMON_HEADERS ); const token = loginRes.token; - appendLog(2, `Authenticated as ${HERMES_USER}`); - updateStep(2, { status: "done" }); + appendLog(5, `Authenticated as ${HERMES_USER}`); + updateStep(5, { status: "done" }); - // ─── Step 3: Start upload ─────────────────────────────────────── - updateStep(3, { status: "running" }); + // ─── Step 6: Start upload ─────────────────────────────────────── + updateStep(6, { status: "running" }); const fileHash = computeFileHash(zipPath); @@ -354,14 +385,14 @@ function App() { ); const { uploadId, urls: partUrls, totalParts, partSize } = startRes; - appendLog(3, `Upload ID: ${uploadId.slice(0, 32)}...`); - appendLog(3, `File hash: ${fileHash}`); - appendLog(3, `File size: ${fileSizeMB} MB`); - appendLog(3, `Parts: ${totalParts}`); - updateStep(3, { status: "done", detail: `${totalParts} parts` }); + appendLog(6, `Upload ID: ${uploadId.slice(0, 32)}...`); + appendLog(6, `File hash: ${fileHash}`); + appendLog(6, `File size: ${fileSizeMB} MB`); + appendLog(6, `Parts: ${totalParts}`); + updateStep(6, { status: "done", detail: `${totalParts} parts` }); - // ─── Step 4: Upload parts ─────────────────────────────────────── - updateStep(4, { status: "running", progress: { bytes: 0, totalBytes: fileSize } }); + // ─── Step 7: Upload parts ─────────────────────────────────────── + updateStep(7, { status: "running", progress: { bytes: 0, totalBytes: fileSize } }); // Use the partSize from the server response const completedParts: { partNumber: number; etag: string }[] = []; @@ -376,7 +407,7 @@ function App() { partSize, totalParts, (partBytes) => { - updateStep(4, { + updateStep(7, { status: "running", progress: { bytes: prevPartsBytes + partBytes, totalBytes: fileSize }, }); @@ -403,14 +434,14 @@ function App() { ); } - updateStep(4, { + updateStep(7, { status: "done", detail: `${fileSizeMB} MB uploaded`, progress: undefined, }); - // ─── Step 5: Complete ──────────────────────────────────────────── - updateStep(5, { status: "running" }); + // ─── Step 8: Complete ──────────────────────────────────────────── + updateStep(8, { status: "running" }); await apiPost( `${BASE_URL}/api/uploads/complete`, @@ -422,11 +453,11 @@ function App() { authHeaders(token) ); - appendLog(5, `Key: ${UPLOAD_KEY}`); - updateStep(5, { status: "done" }); + appendLog(8, `Key: ${UPLOAD_KEY}`); + updateStep(8, { status: "done" }); - // ─── Step 6: Upload install-galaxy.sh ──────────────────────────── - updateStep(6, { status: "running" }); + // ─── Step 9: Upload install-galaxy.sh ──────────────────────────── + updateStep(9, { status: "running" }); const installScriptPath = path.join(WORKSPACE_ROOT, "script", "install-galaxy.sh"); const scriptFileSize = statSync(installScriptPath).size; @@ -446,7 +477,7 @@ function App() { const scriptCompletedParts: { partNumber: number; etag: string }[] = []; let scriptBytesUploaded = 0; - updateStep(6, { status: "running", progress: { bytes: 0, totalBytes: scriptFileSize } }); + updateStep(9, { status: "running", progress: { bytes: 0, totalBytes: scriptFileSize } }); for (const part of scriptStartRes.urls) { const prevBytes = scriptBytesUploaded; @@ -457,7 +488,7 @@ function App() { scriptStartRes.partSize, scriptStartRes.totalParts, (partBytes) => { - updateStep(6, { + updateStep(9, { status: "running", progress: { bytes: prevBytes + partBytes, totalBytes: scriptFileSize }, }); @@ -492,8 +523,8 @@ function App() { authHeaders(token) ); - appendLog(6, `Key: ${INSTALL_SCRIPT_KEY}`); - updateStep(6, { status: "done", detail: `${(scriptFileSize / 1024).toFixed(1)} KB`, progress: undefined }); + appendLog(9, `Key: ${INSTALL_SCRIPT_KEY}`); + updateStep(9, { status: "done", detail: `${(scriptFileSize / 1024).toFixed(1)} KB`, progress: undefined }); // Cleanup zip execSync(`rm -f "${zipPath}"`, { stdio: "pipe" }); @@ -542,4 +573,4 @@ function App() { ); } -render(); \ No newline at end of file +render(); diff --git a/script/build-and-install-to-applications/src/index.tsx b/script/build-and-install-to-applications/src/index.tsx index 14667e6f..d10bf0ee 100644 --- a/script/build-and-install-to-applications/src/index.tsx +++ b/script/build-and-install-to-applications/src/index.tsx @@ -150,8 +150,12 @@ function StepLine({ step }: { step: Step }) { function App() { const [steps, setSteps] = useState([ - { label: "Build Galaxy", status: "pending" }, + { label: "Check working directory", status: "pending" }, + { label: "Pull current branch", status: "pending" }, + { label: "Clean Cargo build artifacts", status: "pending" }, + { label: "Bundle Galaxy", status: "pending" }, { label: "Copy to /Applications", status: "pending" }, + { label: "Launch Galaxy", status: "pending" }, ]); const updateStep = useCallback((index: number, update: Partial) => { @@ -172,48 +176,74 @@ function App() { (async () => { try { const appName = "Galaxy.app"; - const appCrateDir = path.join(WORKSPACE_ROOT, "app"); const bundleDir = path.join(WORKSPACE_ROOT, "target/debug/bundle/osx"); const appPath = path.join(bundleDir, appName); const destPath = path.join("/Applications", appName); - // ─── Step 0: Build ────────────────────────────────────────────── + // ─── Step 0: Check working directory ─────────────────────────── updateStep(0, { status: "running" }); - - const buildCmd = "cargo bundle --bin galaxy-oss"; - - await runCommandStreaming(buildCmd, appCrateDir, (line) => appendLog(0, line)); + await runCommandStreaming( + `if [ -n "$(git status --porcelain)" ]; then git status --short; exit 1; fi`, + WORKSPACE_ROOT, + (line) => appendLog(0, line) + ); updateStep(0, { status: "done" }); - // ─── Step 1: Copy to /Applications ───────────────────────────── + // ─── Step 1: Pull current branch ──────────────────────────────── updateStep(1, { status: "running" }); + const branch = execSync("git branch --show-current", { + cwd: WORKSPACE_ROOT, + encoding: "utf8", + }).trim(); + if (!branch) throw new Error("Cannot install from a detached HEAD"); + await runCommandStreaming( + `git pull --ff-only origin "${branch}"`, + WORKSPACE_ROOT, + (line) => appendLog(1, line) + ); + updateStep(1, { status: "done", detail: branch }); + + // ─── Step 2: Clean ────────────────────────────────────────────── + updateStep(2, { status: "running" }); + await runCommandStreaming("cargo clean", WORKSPACE_ROOT, (line) => appendLog(2, line)); + updateStep(2, { status: "done" }); + + // ─── Step 3: Bundle ───────────────────────────────────────────── + updateStep(3, { status: "running" }); + await runCommandStreaming( + "cargo bundle --bin galaxy-oss --package galaxy", + WORKSPACE_ROOT, + (line) => appendLog(3, line) + ); + updateStep(3, { status: "done" }); + + // ─── Step 4: Copy to /Applications ───────────────────────────── + updateStep(4, { status: "running" }); if (!fs.existsSync(appPath)) { throw new Error(`Built app not found at ${appPath}`); } - await stopApp("Galaxy", (line) => appendLog(1, line)); + await stopApp("Galaxy", (line) => appendLog(4, line)); if (fs.existsSync(destPath)) { - appendLog(1, `Removing existing ${destPath}`); + appendLog(4, `Removing existing ${destPath}`); execSync(`rm -rf "${destPath}"`, { stdio: "pipe" }); } - appendLog(1, `Copying ${appPath} → ${destPath}`); + appendLog(4, `Copying ${appPath} → ${destPath}`); await runCommandStreaming( `ditto "${appPath}" "${destPath}"`, WORKSPACE_ROOT, - (line) => appendLog(1, line) + (line) => appendLog(4, line) ); - updateStep(1, { status: "done", detail: destPath }); + updateStep(4, { status: "done", detail: destPath }); - // ─── Step 2: Launch ──────────────────────────────────────────── - setSteps((prev) => [...prev, { label: "Launch Galaxy", status: "running" }]); + // ─── Step 5: Launch ──────────────────────────────────────────── + updateStep(5, { status: "running" }); execSync(`open "${destPath}"`, { stdio: "ignore" }); - setSteps((prev) => - prev.map((s, i) => (i === 2 ? { ...s, status: "done" } : s)) - ); + updateStep(5, { status: "done" }); } catch (err: any) { setSteps((prev) => prev.map((s) => diff --git a/script/build-and-upload-hermes.sh b/script/build-and-upload-hermes.sh new file mode 100755 index 00000000..69351c66 --- /dev/null +++ b/script/build-and-upload-hermes.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Compatibility entrypoint for building and uploading Galaxy to Hermes. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/build-and-deploy-hermes.sh" "$@" diff --git a/script/install-galaxy.sh b/script/install-galaxy.sh index c4677292..5467996b 100755 --- a/script/install-galaxy.sh +++ b/script/install-galaxy.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# install-galaxy.sh — Download, sign, and install Galaxy.app on macOS. +# install-galaxy.sh — Download and install Galaxy.app on macOS. # # Usage: # curl -fsSL https://mng-web-sharing.mini-games.tv/wst-data/ryan-share/galaxy/install-galaxy.sh | bash @@ -44,15 +44,9 @@ if [[ ! -d "$TMP_DIR/$APP_NAME" ]]; then fail "$APP_NAME not found after extraction." fi -# ---------- 3. Clear quarantine & ad-hoc sign ---------- -info "Clearing quarantine attributes..." -/usr/bin/xattr -cr "$TMP_DIR/$APP_NAME" || fail "Failed to clear quarantine attributes." - -info "Ad-hoc code signing..." -/usr/bin/codesign --force --deep --sign - "$TMP_DIR/$APP_NAME" || fail "Code signing failed." - -info "Verifying code signature..." -/usr/bin/codesign --verify --deep --strict --verbose=2 "$TMP_DIR/$APP_NAME" || fail "Code signature verification failed." +# ---------- 3. Clear quarantine ---------- +info "Removing the macOS quarantine attribute..." +/usr/bin/xattr -dr com.apple.quarantine "$TMP_DIR/$APP_NAME" 2>/dev/null || true # ---------- 4. Kill, remove, install, launch ---------- info "Stopping any running Galaxy processes..." @@ -66,9 +60,6 @@ info "Staging $APP_NAME in $INSTALL_DIR..." rm -rf "$STAGED_APP" cp -R "$TMP_DIR/$APP_NAME" "$STAGED_APP" || fail "Failed to copy $APP_NAME to $INSTALL_DIR." -/usr/bin/codesign --verify --deep --strict --verbose=2 "$STAGED_APP" || - fail "Installed application signature verification failed." - if [[ -d "$INSTALLED_APP" ]]; then info "Removing existing $INSTALLED_APP..." rm -rf "$INSTALLED_APP"