Repository navigation
Fix nightly SSH remote daemon checksum mismatch #2225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -317,19 +317,48 @@ jobs: | |
| if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') | ||
| run: | | ||
| set -euo pipefail | ||
| # Build with --asset-suffix so manifest download URLs point to | ||
| # immutable, build-specific asset names (e.g. cmuxd-remote-darwin-arm64-2362248028801). | ||
| # This prevents checksum mismatches when a newer nightly overwrites | ||
| # the shared "latest" assets on the release. | ||
| ./scripts/build_remote_daemon_release_assets.sh \ | ||
| --version "$NIGHTLY_REMOTE_DAEMON_VERSION" \ | ||
| --release-tag "nightly" \ | ||
| --repo "manaflow-ai/cmux" \ | ||
| --output-dir "remote-daemon-assets" | ||
| MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' remote-daemon-assets/cmuxd-remote-manifest.json)" | ||
| --output-dir "remote-daemon-assets" \ | ||
| --asset-suffix "$NIGHTLY_BUILD" | ||
| MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json")" | ||
| APP_PLIST="build-universal/Build/Products/Release/cmux NIGHTLY.app/Contents/Info.plist" | ||
| if [ ! -f "$APP_PLIST" ]; then | ||
| echo "Missing nightly app Info.plist at $APP_PLIST" >&2 | ||
| exit 1 | ||
| fi | ||
| plutil -remove CMUXRemoteDaemonManifestJSON "$APP_PLIST" >/dev/null 2>&1 || true | ||
| plutil -insert CMUXRemoteDaemonManifestJSON -string "$MANIFEST_JSON" "$APP_PLIST" | ||
| # Also create unsuffixed "latest" copies for the release page and | ||
| # any tooling that fetches the generic asset names. The manifest's | ||
| # downloadURLs still point to the versioned filenames (intentional: | ||
| # the live manifest is used by the client-side checksum fallback | ||
| # which only reads sha256, not downloadURL). The unsuffixed copies | ||
| # are convenience aliases and don't carry build-provenance | ||
| # attestation (attested versioned files are canonical). | ||
| for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do | ||
| cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \ | ||
| "remote-daemon-assets/cmuxd-remote-${platform}" | ||
| done | ||
| # Regenerate unsuffixed checksums with generic filenames so | ||
| # `shasum -c cmuxd-remote-checksums.txt` works against the aliases. | ||
| ( | ||
| cd remote-daemon-assets | ||
| shasum -a 256 \ | ||
| cmuxd-remote-darwin-arm64 \ | ||
| cmuxd-remote-darwin-amd64 \ | ||
| cmuxd-remote-linux-arm64 \ | ||
| cmuxd-remote-linux-amd64 \ | ||
| > cmuxd-remote-checksums.txt | ||
| ) | ||
| cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \ | ||
| "remote-daemon-assets/cmuxd-remote-manifest.json" | ||
|
Comment on lines
324
to
+361
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't suffix remote-daemon URLs for non-publishing builds. This block still runs on branch builds, but those runs only upload workflow artifacts. Since Possible fix- ./scripts/build_remote_daemon_release_assets.sh \
- --version "$NIGHTLY_REMOTE_DAEMON_VERSION" \
- --release-tag "nightly" \
- --repo "manaflow-ai/cmux" \
- --output-dir "remote-daemon-assets" \
- --asset-suffix "$NIGHTLY_BUILD"
- MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json")"
+ BUILD_REMOTE_DAEMON_ARGS=(
+ --version "$NIGHTLY_REMOTE_DAEMON_VERSION"
+ --release-tag "nightly"
+ --repo "manaflow-ai/cmux"
+ --output-dir "remote-daemon-assets"
+ )
+ MANIFEST_PATH="remote-daemon-assets/cmuxd-remote-manifest.json"
+ if [ "${{ needs.decide.outputs.should_publish }}" = "true" ]; then
+ BUILD_REMOTE_DAEMON_ARGS+=(--asset-suffix "$NIGHTLY_BUILD")
+ MANIFEST_PATH="remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json"
+ fi
+ ./scripts/build_remote_daemon_release_assets.sh "${BUILD_REMOTE_DAEMON_ARGS[@]}"
+ MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "$MANIFEST_PATH")"
@@
- for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do
- cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \
- "remote-daemon-assets/cmuxd-remote-${platform}"
- done
- (
- cd remote-daemon-assets
- shasum -a 256 \
- cmuxd-remote-darwin-arm64 \
- cmuxd-remote-darwin-amd64 \
- cmuxd-remote-linux-arm64 \
- cmuxd-remote-linux-amd64 \
- > cmuxd-remote-checksums.txt
- )
- cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \
- "remote-daemon-assets/cmuxd-remote-manifest.json"
+ if [ "${{ needs.decide.outputs.should_publish }}" = "true" ]; then
+ for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do
+ cp "remote-daemon-assets/cmuxd-remote-${platform}-${NIGHTLY_BUILD}" \
+ "remote-daemon-assets/cmuxd-remote-${platform}"
+ done
+ (
+ cd remote-daemon-assets
+ shasum -a 256 \
+ cmuxd-remote-darwin-arm64 \
+ cmuxd-remote-darwin-amd64 \
+ cmuxd-remote-linux-arm64 \
+ cmuxd-remote-linux-amd64 \
+ > cmuxd-remote-checksums.txt
+ )
+ cp "remote-daemon-assets/cmuxd-remote-manifest-${NIGHTLY_BUILD}.json" \
+ "remote-daemon-assets/cmuxd-remote-manifest.json"
+ fiAlso applies to: 511-516 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not actionable. Branch builds only upload to workflow artifacts for CI validation, never installed by users or distributed via Sparkle. The manifest URLs don't matter for those artifacts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🧠 Learnings used |
||
|
|
||
| - name: Import signing cert | ||
| if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') | ||
|
|
@@ -479,12 +508,12 @@ jobs: | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | ||
| with: | ||
| subject-path: | | ||
| remote-daemon-assets/cmuxd-remote-darwin-arm64 | ||
| remote-daemon-assets/cmuxd-remote-darwin-amd64 | ||
| remote-daemon-assets/cmuxd-remote-linux-arm64 | ||
| remote-daemon-assets/cmuxd-remote-linux-amd64 | ||
| remote-daemon-assets/cmuxd-remote-checksums.txt | ||
| remote-daemon-assets/cmuxd-remote-manifest.json | ||
| remote-daemon-assets/cmuxd-remote-darwin-arm64-${{ env.NIGHTLY_BUILD }} | ||
| remote-daemon-assets/cmuxd-remote-darwin-amd64-${{ env.NIGHTLY_BUILD }} | ||
| remote-daemon-assets/cmuxd-remote-linux-arm64-${{ env.NIGHTLY_BUILD }} | ||
| remote-daemon-assets/cmuxd-remote-linux-amd64-${{ env.NIGHTLY_BUILD }} | ||
| remote-daemon-assets/cmuxd-remote-checksums-${{ env.NIGHTLY_BUILD }}.txt | ||
| remote-daemon-assets/cmuxd-remote-manifest-${{ env.NIGHTLY_BUILD }}.json | ||
|
Comment on lines
510
to
+516
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The If build provenance isn't required for the "latest" aliases (and users who care about provenance are expected to use the versioned filenames), this is fine — just worth an explicit comment so it doesn't look like an oversight. If provenance on the generic names is desired, the unsuffixed copies should be added to |
||
|
|
||
| - name: Upload branch nightly artifacts | ||
| if: needs.decide.outputs.should_publish != 'true' | ||
|
|
@@ -494,12 +523,7 @@ jobs: | |
| path: | | ||
| cmux-nightly-macos*.dmg | ||
| appcast.xml | ||
| remote-daemon-assets/cmuxd-remote-darwin-arm64 | ||
| remote-daemon-assets/cmuxd-remote-darwin-amd64 | ||
| remote-daemon-assets/cmuxd-remote-linux-arm64 | ||
| remote-daemon-assets/cmuxd-remote-linux-amd64 | ||
| remote-daemon-assets/cmuxd-remote-checksums.txt | ||
| remote-daemon-assets/cmuxd-remote-manifest.json | ||
| remote-daemon-assets/cmuxd-remote-* | ||
| appcast-universal.xml | ||
| if-no-files-found: error | ||
|
|
||
|
|
@@ -533,12 +557,7 @@ jobs: | |
| cmux-nightly-macos-${{ github.run_id }}*.dmg | ||
| cmux-nightly-macos.dmg | ||
| appcast.xml | ||
| remote-daemon-assets/cmuxd-remote-darwin-arm64 | ||
| remote-daemon-assets/cmuxd-remote-darwin-amd64 | ||
| remote-daemon-assets/cmuxd-remote-linux-arm64 | ||
| remote-daemon-assets/cmuxd-remote-linux-amd64 | ||
| remote-daemon-assets/cmuxd-remote-checksums.txt | ||
| remote-daemon-assets/cmuxd-remote-manifest.json | ||
| remote-daemon-assets/cmuxd-remote-* | ||
| appcast-universal.xml | ||
| overwrite_files: true | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4146,7 +4146,29 @@ final class WorkspaceRemoteSessionController { | |
| .appendingPathComponent("cmuxd-remote", isDirectory: false) | ||
| } | ||
|
|
||
| private func downloadRemoteDaemonBinaryLocked(entry: WorkspaceRemoteDaemonManifest.Entry, version: String) throws -> URL { | ||
| /// Fetch the live manifest JSON from the release, returning nil on any failure. | ||
| private static func fetchRemoteManifestLocked(releaseURL: String, version: String) -> WorkspaceRemoteDaemonManifest? { | ||
| guard let manifestURL = URL(string: "\(releaseURL)/cmuxd-remote-manifest.json") else { return nil } | ||
|
Comment on lines
+4149
to
+4151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C3 'releaseURL|checksumsURL|downloadURL|cmuxd-remote-manifest\.json|releases/(tag|download)' \
-g '!**/.build/**' \
-g '!**/DerivedData/**' \
-g '!**/node_modules/**'Repository: manaflow-ai/cmux Length of output: 29894 Derive the live-manifest URL from a confirmed asset URL. The manifest builder correctly sets - guard let manifestURL = URL(string: "\(releaseURL)/cmuxd-remote-manifest.json") else { return nil }
+ guard let assetURL = URL(string: releaseURL) else { return nil }
+ let manifestURL = assetURL.deletingLastPathComponent()
+ .appendingPathComponent("cmuxd-remote-manifest.json")At the call site, pass - let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion, releaseURL: manifest.releaseURL)
+ let downloadedURL = try downloadRemoteDaemonBinaryLocked(
+ entry: entry,
+ version: manifest.appVersion,
+ releaseURL: manifest.checksumsURL
+ )Also applies to: 4171, 4218–4225, 4265 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a real issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
🧠 Learnings used |
||
| let request = NSMutableURLRequest(url: manifestURL) | ||
| request.timeoutInterval = 15 | ||
| request.setValue("cmux/\(version)", forHTTPHeaderField: "User-Agent") | ||
| let session = URLSession(configuration: .ephemeral) | ||
| let semaphore = DispatchSemaphore(value: 0) | ||
| var resultData: Data? | ||
| session.dataTask(with: request as URLRequest) { data, response, error in | ||
| defer { semaphore.signal() } | ||
| guard error == nil, | ||
| let httpResponse = response as? HTTPURLResponse, | ||
| (200...299).contains(httpResponse.statusCode) else { return } | ||
| resultData = data | ||
| }.resume() | ||
| _ = semaphore.wait(timeout: .now() + 20.0) | ||
| session.finishTasksAndInvalidate() | ||
| guard let data = resultData else { return nil } | ||
| return try? JSONDecoder().decode(WorkspaceRemoteDaemonManifest.self, from: data) | ||
| } | ||
|
|
||
| private func downloadRemoteDaemonBinaryLocked(entry: WorkspaceRemoteDaemonManifest.Entry, version: String, releaseURL: String? = nil) throws -> URL { | ||
| guard let url = URL(string: entry.downloadURL) else { | ||
| throw NSError(domain: "cmux.remote.daemon", code: 25, userInfo: [ | ||
| NSLocalizedDescriptionKey: "remote daemon manifest has an invalid download URL", | ||
|
|
@@ -4193,10 +4215,21 @@ final class WorkspaceRemoteSessionController { | |
| } | ||
|
|
||
| let downloadedSHA = try Self.sha256Hex(forFile: downloadedURL) | ||
| guard downloadedSHA == entry.sha256.lowercased() else { | ||
| throw NSError(domain: "cmux.remote.daemon", code: 28, userInfo: [ | ||
| NSLocalizedDescriptionKey: "remote daemon checksum mismatch for \(entry.assetName)", | ||
| ]) | ||
| if downloadedSHA != entry.sha256.lowercased() { | ||
| // The embedded manifest's checksum doesn't match the downloaded binary. | ||
| // This can happen when a newer nightly overwrites the shared release | ||
| // asset after this build's manifest was embedded. As a fallback, fetch | ||
| // the live manifest from the release and verify against that. | ||
| if let releaseURL, | ||
| let liveManifest = Self.fetchRemoteManifestLocked(releaseURL: releaseURL, version: version), | ||
| let liveEntry = liveManifest.entry(goOS: entry.goOS, goArch: entry.goArch), | ||
| downloadedSHA == liveEntry.sha256.lowercased() { | ||
| debugLog("remote.download.checksum-fallback: embedded manifest checksum stale, live manifest matched for \(entry.assetName)") | ||
| } else { | ||
| throw NSError(domain: "cmux.remote.daemon", code: 28, userInfo: [ | ||
| NSLocalizedDescriptionKey: "remote daemon checksum mismatch for \(entry.assetName)", | ||
| ]) | ||
| } | ||
| } | ||
|
|
||
| let tempURL = cacheURL.deletingLastPathComponent() | ||
|
|
@@ -4229,7 +4262,7 @@ final class WorkspaceRemoteSessionController { | |
| } | ||
| try? FileManager.default.removeItem(at: cacheURL) | ||
| } | ||
| let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion) | ||
| let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion, releaseURL: manifest.releaseURL) | ||
| debugLog("remote.build.downloaded path=\(downloadedURL.path)") | ||
| return downloadedURL | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
downloadURLpoints to versioned assetsThe unsuffixed
cmuxd-remote-manifest.jsonis a verbatim copy of the suffixed manifest, so everydownloadURLinside it references the versioned asset name (e.g.cmuxd-remote-darwin-arm64-${NIGHTLY_BUILD}), not the genericcmuxd-remote-darwin-arm64.The Swift fallback in
fetchRemoteManifestLockedis unaffected — it only reads thesha256field from the live manifest, neverdownloadURL. However, any external tooling or users who fetchcmuxd-remote-manifest.jsonand follow itsdownloadURLfields to find the "latest" binary will be silently redirected to a versioned filename on every build. This may be intentional, but it's worth documenting in the comment on line 338 so future maintainers don't try to "fix" it.