Skip to content

Release reproducibility: pin the full build graph and publish one verified artifact #155

Description

@manishiitg

Problem

A desktop tag is not currently a reproducible release boundary. The same builder tag can resolve different dependency source and install a different MCP bridge later.

Confirmed findings

P0: release dependencies are taken from moving branches

.github/workflows/desktop-release.yml checks out manishiitg/mcpagent and manishiitg/multi-llm-provider-go without a ref, then rewrites agent_go/go.mod to those checkout directories. Both resolve to whatever their main branch contains when the workflow runs, not what the builder tag declared. A retry of one tag can build different binaries.

P0: the installer builds MCP bridge from moving main

install.sh downloads mcpagent and the provider from refs/heads/main.tar.gz. Two users installing the same DMG version at different times can receive different mcpbridge binaries that may not match the bundled server.

P1: build and publish are one mutable operation

The tag job rebuilds and publishes through electron-builder. A retry is another build rather than publication of an already verified immutable artifact. No release manifest binds source/dependency SHAs to the assets.

P1: release script can publish unreviewed local main

scripts/desktop-release.sh allows local main to be ahead of origin/main, pushes it, and tags it. It does not require HEAD == origin/main, validate the canonical origin URL, enforce a monotonic version, or require package/package-lock versions to match the tag before tagging.

P1: assets have no integrity/provenance bundle

Releases do not publish SHA-256 checksums, an SBOM, a machine-readable build manifest, or GitHub artifact attestation/provenance. The updater downloads a known filename but does not verify a release-published checksum.

P2: toolchain and Actions inputs float

go-version: 1.25.x, node-version: 20, macos-15-intel, and action major tags can change. npm dependencies are lockfile-controlled, but the OS/toolchain/build-action graph is not fully pinned. The unsigned/unnotarized DMG is also a distribution-security gap, though signing is separable from deterministic source selection.

P2: documentation describes a second manual release path

desktop/README.md still documents manual bump/tag/publish steps alongside the dedicated script, increasing the chance that releases bypass preflight and changelog behavior.

Recommended design

  1. Add a committed release/build manifest containing builder SHA, exact mcpagent SHA, exact provider SHA, Go/Node/Electron versions, architecture, and version.
  2. Make CI checkout dependencies at those exact SHAs. Never resolve dependency main during a tag build.
  3. Split build from publication: build once, test/inspect it, upload an immutable workflow artifact, then attach exactly those files to the GitHub release. Retries of publication must not rebuild.
  4. Publish SHA256SUMS, SBOMs, the build manifest, and GitHub artifact attestations. Verify the DMG checksum in the updater/installer.
  5. Version or bundle mcpbridge with the release. Do not compile it from branch heads during installation.
  6. Make the release script require clean main, canonical origin, HEAD == origin/main, no existing tag/release, a version greater than Latest, and matching desktop package/package-lock versions. Add --dry-run.
  7. Collapse README guidance to the release script and recovery instructions.
  8. Pin toolchain patch versions and action commit SHAs. Record the runner image in the manifest.

Suggested implementation phases

Phase 1: source reproducibility

  • build manifest and exact dependency refs
  • strict release preflight
  • versioned/bundled mcpbridge
  • build once, publish same artifact

Phase 2: integrity and supply chain

  • checksums, SBOM, attestations
  • updater checksum verification
  • pin actions/toolchain

Phase 3: distribution hardening

  • Developer ID signing and notarization
  • installation test on a clean macOS VM

Tests / acceptance

  • rerunning a release from the same manifest after dependency main branches move produces the same server/frontend payload hashes
  • publication retry reuses the original workflow artifact and does not invoke a build
  • release fails before tagging when local main differs from origin, version is not greater than Latest, dependency SHAs are missing, or package versions disagree
  • release contains manifest, checksums, SBOM, attestations, expected DMG/zip/update metadata, and exact source links
  • installer obtains the bridge matching the release manifest and verifies its checksum

Current evidence

  • .github/workflows/desktop-release.yml: dependency checkouts have no refs; go mod edit -replace points to those moving checkouts
  • agent_go/go.mod: declares versions but also has local replace directives
  • scripts/desktop-release.sh: permits local-main-ahead push/tag flow
  • install.sh: downloads both bridge source repositories from main
  • desktop/README.md: parallel manual release path

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions