Merge origin/main into issue-151-ssh-remote-port-proxying

This commit is contained in:
Lawrence Chen 2026-02-28 17:17:30 -08:00
commit c179ee74ea
202 changed files with 54781 additions and 2314 deletions

View file

@ -2,10 +2,50 @@
set -euo pipefail
# Build, sign, notarize, create DMG, generate appcast, and upload to GitHub release.
# Usage: ./scripts/build-sign-upload.sh <tag>
# Usage: ./scripts/build-sign-upload.sh <tag> [--allow-overwrite]
# Requires: source ~/.secrets/cmuxterm.env && export SPARKLE_PRIVATE_KEY
TAG="${1:?Usage: $0 <tag>}"
usage() {
cat <<'EOF'
Usage: ./scripts/build-sign-upload.sh <tag> [--allow-overwrite]
Options:
--allow-overwrite Permit replacing existing release assets for the same tag.
Use only for emergency rerolls.
EOF
}
ALLOW_OVERWRITE="false"
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "$1" in
--allow-overwrite)
ALLOW_OVERWRITE="true"
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL[@]}"
if [[ $# -ne 1 ]]; then
usage >&2
exit 1
fi
TAG="$1"
SIGN_HASH="A050CC7E193C8221BDBA204E731B046CDCCC1B30"
ENTITLEMENTS="cmux.entitlements"
APP_PATH="build/Build/Products/Release/cmux.app"
@ -81,8 +121,29 @@ echo "Generating appcast..."
# --- Create GitHub release (if needed) and upload ---
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Uploading to existing release $TAG..."
gh release upload "$TAG" cmux-macos.dmg appcast.xml --clobber
echo "Release $TAG already exists"
EXISTING_ASSETS="$(gh release view "$TAG" --json assets --jq '.assets[].name' || true)"
HAS_CONFLICTING_ASSET="false"
for asset in cmux-macos.dmg appcast.xml; do
if printf '%s\n' "$EXISTING_ASSETS" | grep -Fxq "$asset"; then
HAS_CONFLICTING_ASSET="true"
break
fi
done
if [[ "$HAS_CONFLICTING_ASSET" == "true" && "$ALLOW_OVERWRITE" != "true" ]]; then
echo "ERROR: Refusing to overwrite signed release assets for existing tag $TAG." >&2
echo "Use a new tag, or rerun with --allow-overwrite for an emergency reroll." >&2
exit 1
fi
if [[ "$ALLOW_OVERWRITE" == "true" ]]; then
echo "Uploading with overwrite enabled for existing release $TAG..."
gh release upload "$TAG" cmux-macos.dmg appcast.xml --clobber
else
echo "Uploading to existing release $TAG..."
gh release upload "$TAG" cmux-macos.dmg appcast.xml
fi
else
echo "Creating release $TAG and uploading..."
gh release create "$TAG" cmux-macos.dmg appcast.xml --title "$TAG" --notes "See CHANGELOG.md for details"

View file

@ -0,0 +1,37 @@
"use strict";
const IMMUTABLE_RELEASE_ASSETS = ["cmux-macos.dmg", "appcast.xml"];
const RELEASE_ASSET_GUARD_STATE = Object.freeze({
CLEAR: "clear",
PARTIAL: "partial",
COMPLETE: "complete",
});
function evaluateReleaseAssetGuard({ existingAssetNames, immutableAssetNames = IMMUTABLE_RELEASE_ASSETS }) {
const immutableAssets = immutableAssetNames || IMMUTABLE_RELEASE_ASSETS;
const existing = new Set(existingAssetNames || []);
const conflicts = immutableAssets.filter((assetName) => existing.has(assetName));
const missingImmutableAssets = immutableAssets.filter((assetName) => !existing.has(assetName));
let guardState = RELEASE_ASSET_GUARD_STATE.CLEAR;
if (conflicts.length === immutableAssets.length && immutableAssets.length > 0) {
guardState = RELEASE_ASSET_GUARD_STATE.COMPLETE;
} else if (conflicts.length > 0) {
guardState = RELEASE_ASSET_GUARD_STATE.PARTIAL;
}
return {
conflicts,
missingImmutableAssets,
guardState,
hasPartialConflict: guardState === RELEASE_ASSET_GUARD_STATE.PARTIAL,
shouldSkipBuildAndUpload: guardState === RELEASE_ASSET_GUARD_STATE.COMPLETE,
shouldSkipUpload: guardState === RELEASE_ASSET_GUARD_STATE.COMPLETE,
};
}
module.exports = {
IMMUTABLE_RELEASE_ASSETS,
RELEASE_ASSET_GUARD_STATE,
evaluateReleaseAssetGuard,
};

View file

@ -0,0 +1,49 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const {
IMMUTABLE_RELEASE_ASSETS,
RELEASE_ASSET_GUARD_STATE,
evaluateReleaseAssetGuard,
} = require("./release_asset_guard");
test("marks guard as complete and skips build/upload when all immutable assets already exist", () => {
const result = evaluateReleaseAssetGuard({
existingAssetNames: ["cmux-macos.dmg", "appcast.xml", "notes.txt"],
});
assert.deepEqual(result.conflicts, IMMUTABLE_RELEASE_ASSETS);
assert.deepEqual(result.missingImmutableAssets, []);
assert.equal(result.guardState, RELEASE_ASSET_GUARD_STATE.COMPLETE);
assert.equal(result.hasPartialConflict, false);
assert.equal(result.shouldSkipBuildAndUpload, true);
assert.equal(result.shouldSkipUpload, true);
});
test("marks guard as clear when immutable assets are not present", () => {
const result = evaluateReleaseAssetGuard({
existingAssetNames: ["notes.txt", "checksums.txt"],
});
assert.deepEqual(result.conflicts, []);
assert.deepEqual(result.missingImmutableAssets, IMMUTABLE_RELEASE_ASSETS);
assert.equal(result.guardState, RELEASE_ASSET_GUARD_STATE.CLEAR);
assert.equal(result.hasPartialConflict, false);
assert.equal(result.shouldSkipBuildAndUpload, false);
assert.equal(result.shouldSkipUpload, false);
});
test("marks guard as partial when only some immutable assets exist", () => {
const result = evaluateReleaseAssetGuard({
existingAssetNames: ["appcast.xml"],
});
assert.deepEqual(result.conflicts, ["appcast.xml"]);
assert.deepEqual(result.missingImmutableAssets, ["cmux-macos.dmg"]);
assert.equal(result.guardState, RELEASE_ASSET_GUARD_STATE.PARTIAL);
assert.equal(result.hasPartialConflict, true);
assert.equal(result.shouldSkipBuildAndUpload, false);
assert.equal(result.shouldSkipUpload, false);
});

View file

@ -13,6 +13,7 @@ cd "$(dirname "$0")/.."
DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData/cmux-tests-v1"
APP="$DERIVED_DATA_PATH/Build/Products/Debug/cmux DEV.app"
RUN_TAG="tests-v1"
echo "== build =="
# Work around stale explicit-module cache artifacts (notably Sentry headers) that can
@ -51,7 +52,7 @@ launch_and_wait() {
defaults write com.cmuxterm.app.debug socketControlMode -string full >/dev/null 2>&1 || true
# Launch directly with UI test mode enabled so startup follows deterministic test codepaths.
CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/dev/null 2>&1 &
CMUX_TAG="$RUN_TAG" CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/dev/null 2>&1 &
SOCK=""
for _ in {1..120}; do
@ -70,7 +71,7 @@ launch_and_wait() {
export CMUX_SOCKET="$SOCK"
# Ensure LaunchServices has a visible/main window attached for rendering checks.
open "$APP" >/dev/null 2>&1 || true
CMUX_TAG="$RUN_TAG" open "$APP" >/dev/null 2>&1 || true
sleep 0.5
echo "== wait ready =="

View file

@ -13,6 +13,7 @@ cd "$(dirname "$0")/.."
DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData/cmux-tests-v2"
APP="$DERIVED_DATA_PATH/Build/Products/Debug/cmux DEV.app"
RUN_TAG="tests-v2"
echo "== build =="
# Work around stale explicit-module cache artifacts (notably Sentry headers) that can
@ -51,7 +52,7 @@ launch_and_wait() {
defaults write com.cmuxterm.app.debug socketControlMode -string full >/dev/null 2>&1 || true
# Launch directly with UI test mode enabled so startup follows deterministic test codepaths.
CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/dev/null 2>&1 &
CMUX_TAG="$RUN_TAG" CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/dev/null 2>&1 &
SOCK=""
for _ in {1..120}; do
@ -70,7 +71,7 @@ launch_and_wait() {
export CMUX_SOCKET="$SOCK"
# Ensure LaunchServices has a visible/main window attached for rendering checks.
open "$APP" >/dev/null 2>&1 || true
CMUX_TAG="$RUN_TAG" open "$APP" >/dev/null 2>&1 || true
sleep 0.5
echo "== wait ready =="