cmux/scripts/build_remote_daemon_release_assets.sh
Lawrence Chen ccd84bd578
Fix nightly SSH remote daemon checksum mismatch (#2225)
* Fix nightly SSH remote daemon checksum mismatch

Each nightly build overwrites the shared cmuxd-remote-* assets on the
nightly release, but older nightly DMGs have manifests with checksums
from their build time. When a user's nightly is even one build behind,
the downloaded binary doesn't match their embedded manifest.

Two-layer fix:

1. CI: version nightly remote daemon asset names with the build number
   (e.g. cmuxd-remote-darwin-arm64-2362248028801) so each nightly's
   manifest points to immutable files. Unsuffixed "latest" copies are
   still uploaded for tooling compatibility.

2. Client: on checksum mismatch, fetch the live manifest from the
   release and verify against that. This handles users on older
   nightlies that predate the CI fix.

Fixes https://github.com/manaflow-ai/cmux/issues/1745

* Fix unsuffixed checksums file to use generic filenames

Regenerate cmuxd-remote-checksums.txt from the unsuffixed alias
binaries so `shasum -c` works against the generic asset names.
Also document that unsuffixed manifest intentionally keeps versioned
downloadURLs and that aliases don't carry attestation.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
2026-03-26 17:24:37 -07:00

173 lines
4.6 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: scripts/build_remote_daemon_release_assets.sh \
--version <app-version> \
--release-tag <tag> \
--repo <owner/repo> \
--output-dir <dir> \
[--asset-suffix <suffix>]
Builds cmuxd-remote release assets for the supported remote platforms and emits:
cmuxd-remote-<goos>-<goarch>[-<suffix>]
cmuxd-remote-checksums[-<suffix>].txt
cmuxd-remote-manifest[-<suffix>].json
When --asset-suffix is provided, all output filenames and manifest download URLs
include the suffix, making each build's assets immutable (used by nightly builds
to avoid checksum mismatches when assets are overwritten by later builds).
EOF
}
VERSION=""
RELEASE_TAG=""
REPO=""
OUTPUT_DIR=""
ASSET_SUFFIX=""
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
VERSION="${2:-}"
shift 2
;;
--release-tag)
RELEASE_TAG="${2:-}"
shift 2
;;
--repo)
REPO="${2:-}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:-}"
shift 2
;;
--asset-suffix)
ASSET_SUFFIX="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown option $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$VERSION" || -z "$RELEASE_TAG" || -z "$REPO" || -z "$OUTPUT_DIR" ]]; then
echo "error: --version, --release-tag, --repo, and --output-dir are required" >&2
usage
exit 1
fi
if ! command -v go >/dev/null 2>&1; then
echo "error: go is required to build cmuxd-remote release assets" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DAEMON_ROOT="${REPO_ROOT}/daemon/remote"
mkdir -p "$OUTPUT_DIR"
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
rm -f "$OUTPUT_DIR"/cmuxd-remote-* "$OUTPUT_DIR"/cmuxd-remote-checksums.txt "$OUTPUT_DIR"/cmuxd-remote-manifest.json
DAEMON_GO_LDFLAGS="-s -w -X main.version=${VERSION}"
DAEMON_GO_BUILD_ARGS=(
build
-trimpath
-buildvcs=false
-ldflags "$DAEMON_GO_LDFLAGS"
)
SUFFIX_TAG=""
if [[ -n "$ASSET_SUFFIX" ]]; then
SUFFIX_TAG="-${ASSET_SUFFIX}"
fi
CHECKSUMS_ASSET_NAME="cmuxd-remote-checksums${SUFFIX_TAG}.txt"
CHECKSUMS_PATH="${OUTPUT_DIR}/${CHECKSUMS_ASSET_NAME}"
MANIFEST_PATH="${OUTPUT_DIR}/cmuxd-remote-manifest${SUFFIX_TAG}.json"
TARGETS=(
"darwin arm64"
"darwin amd64"
"linux arm64"
"linux amd64"
)
: > "$CHECKSUMS_PATH"
ENTRIES_FILE="$(mktemp "${TMPDIR:-/tmp}/cmuxd-remote-entries.XXXXXX")"
trap 'rm -f "$ENTRIES_FILE"' EXIT
: > "$ENTRIES_FILE"
for target in "${TARGETS[@]}"; do
read -r GOOS GOARCH <<<"$target"
ASSET_NAME="cmuxd-remote-${GOOS}-${GOARCH}${SUFFIX_TAG}"
OUTPUT_PATH="${OUTPUT_DIR}/${ASSET_NAME}"
# Build into a temp path first, then rename (the binary content is the same
# regardless of suffix, so we build once and move).
BUILD_PATH="${OUTPUT_DIR}/cmuxd-remote-${GOOS}-${GOARCH}.build"
(
cd "$DAEMON_ROOT"
GOOS="$GOOS" \
GOARCH="$GOARCH" \
CGO_ENABLED=0 \
go "${DAEMON_GO_BUILD_ARGS[@]}" \
-o "$BUILD_PATH" \
./cmd/cmuxd-remote
)
mv "$BUILD_PATH" "$OUTPUT_PATH"
chmod 755 "$OUTPUT_PATH"
SHA256="$(shasum -a 256 "$OUTPUT_PATH" | awk '{print $1}')"
printf '%s %s\n' "$SHA256" "$ASSET_NAME" >> "$CHECKSUMS_PATH"
printf '%s\t%s\t%s\t%s\n' "$GOOS" "$GOARCH" "$ASSET_NAME" "$SHA256" >> "$ENTRIES_FILE"
done
python3 - <<'PY' "$VERSION" "$RELEASE_TAG" "$REPO" "$CHECKSUMS_ASSET_NAME" "$CHECKSUMS_PATH" "$MANIFEST_PATH" "$ENTRIES_FILE"
import json
import sys
import urllib.parse
from pathlib import Path
version, release_tag, repo, checksums_asset_name, checksums_path, manifest_path, entries_file = sys.argv[1:]
quoted_tag = urllib.parse.quote(release_tag, safe="")
release_url = f"https://github.com/{repo}/releases/download/{quoted_tag}"
checksums_url = f"{release_url}/{urllib.parse.quote(checksums_asset_name, safe='')}"
entries = []
for line in Path(entries_file).read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
go_os, go_arch, asset_name, sha256 = line.split("\t")
entries.append({
"goOS": go_os,
"goArch": go_arch,
"assetName": asset_name,
"downloadURL": f"{release_url}/{urllib.parse.quote(asset_name, safe='')}",
"sha256": sha256,
})
manifest = {
"schemaVersion": 1,
"appVersion": version,
"releaseTag": release_tag,
"releaseURL": release_url,
"checksumsAssetName": checksums_asset_name,
"checksumsURL": checksums_url,
"entries": entries,
}
Path(manifest_path).write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
PY
echo "Built cmuxd-remote assets in ${OUTPUT_DIR}"