-
-
Notifications
You must be signed in to change notification settings - Fork 194
feat(release): Add portable macOS distribution and Homebrew automation #209
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
Conversation
Add a shared resource-root resolver that prioritizes XCODEBUILDMCP_RESOURCE_ROOT, then executable-relative paths, then package-root fallback for npm/source installs. Route manifest and bundled AXe path discovery through the shared resolver so portable distributions and existing workflows use the same deterministic contract. Also export DYLD_FRAMEWORK_PATH for bundled AXe execution and add tests that cover resource-root precedence and bundled framework environment behavior. Co-Authored-By: Claude <noreply@anthropic.com>
Make local AXe source path env-driven with a project-local default and add macOS verification gates for bundled artifacts. Validate codesign signatures for the AXe binary and bundled frameworks, run Gatekeeper assessment on the executable, and fail bundling when verification fails. Also use non-force directory removal where cleanup already guards for existence. Co-Authored-By: Claude <noreply@anthropic.com>
Add portable packaging and verification scripts for macOS tarball distribution, including bin/libexec layout, bundled runtime resources, production dependencies, and artifact checksum generation. Add npm entrypoints for package and verification workflows. Harden AXe bundling for release provenance by defaulting to remote artifacts, keeping local AXe as explicit opt-in, and enforcing signature checks with CLI-safe Gatekeeper handling. Co-Authored-By: Claude <noreply@anthropic.com>
Extend release workflow with per-architecture macOS packaging jobs, universal artifact assembly, and portable artifact verification. Publish arm64, x64, and universal tarballs plus SHA256 files to GitHub Releases. Keep existing npm and MCP registry release flow intact while wiring version output from the release job into portable packaging stages. Co-Authored-By: Claude <noreply@anthropic.com>
Add a Homebrew formula generator script and release workflow automation that computes artifact SHAs, generates Formula/xcodebuildmcp.rb, and opens a PR in cameroncooke/homebrew-xcodebuildmcp when credentials are configured. Keep the tap update flow token-gated and best-effort so releases continue when tap credentials are unavailable. Co-Authored-By: Claude <noreply@anthropic.com>
Download the Node runtime for the requested target architecture instead of\ncopying the host runtime so x64 packaging and universal lipo assembly\nwork reliably from arm64 builders.\n\nInstall portable archive contents at formula prefix to preserve wrapper\nresource-root path resolution during Homebrew installs.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Smithery capability scanning runs a CJS bundle in CI. Accessing fileURLToPath(importMetaUrl) without a guard can throw when import.meta is present but does not include a usable url, which broke the build-and-test job. Guard the import-meta candidate so package-root discovery still works via process.cwd() and argv fallbacks in CJS environments. Co-Authored-By: Claude <noreply@anthropic.com>
Rename the section heading to avoid the docs command validator parsing "is now" as a CLI subcommand reference in MIGRATION_V2. Co-Authored-By: Claude <noreply@anthropic.com>
commit: |
Restrict docs command validation to fenced and inline code snippets so prose headings are not misclassified as CLI invocations. Restore the original migration heading text now that scanner behavior is context-aware. Co-Authored-By: Claude <noreply@anthropic.com>
Enable update_homebrew_tap for manual test runs so Homebrew automation can be validated in isolation without tag releases. The workflow_dispatch path still skips npm publish and GitHub release creation because those steps remain push-only. Co-Authored-By: Claude <noreply@anthropic.com>
Replace secrets context in step if expressions with env-based checks so workflow_dispatch validation succeeds and isolated Homebrew tests can run. Co-Authored-By: Claude <noreply@anthropic.com>
Use an explicit npm tag during manual dry-run publish so prerelease test versions do not fail workflow_dispatch runs. Co-Authored-By: Claude <noreply@anthropic.com>
The configured macos-13 hosted runner is unavailable in this environment. Build both arm64 and x64 portable artifacts on macos-14, which is valid now that packaging fetches target-arch Node runtimes. Co-Authored-By: Claude <noreply@anthropic.com>
Resolve portable artifact roots dynamically after download to avoid path layout mismatches in the universal packaging job. Bootstrap an empty Homebrew tap by creating Formula/, committing to the default branch, and exiting cleanly. For non-empty repos, detect default branch for PR base and keep formula updates on version branch. Co-Authored-By: Claude <noreply@anthropic.com>
Untar per-arch portable archives into deterministic unpack roots before running universal packaging. This avoids runtime-root resolution issues from artifact directory layout differences. Co-Authored-By: Claude <noreply@anthropic.com>
Allow formula generation to accept a configurable base URL. For workflow_dispatch runs, publish portable archives into the tap repo and point formula URLs at raw artifact paths so Homebrew install can be tested end-to-end without creating GitHub releases or publishing to npm. Co-Authored-By: Claude <noreply@anthropic.com>
Make portable launcher wrappers resolve symlink targets before deriving resource paths. This keeps libexec lookup correct when binaries are invoked via /opt/homebrew/bin symlinks. Co-Authored-By: Claude <noreply@anthropic.com>
Use AXe's Homebrew-specific unsigned archive when available and fall back\nto the legacy signed archive for older releases. This aligns the\nbundling pipeline with AXe's Homebrew packaging and reduces signature\nnoise during downstream install flows.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
AXe's Homebrew artifact is intentionally unsigned, so Gatekeeper assessment\nreliably fails during CI bundling. Skip only that check for the unsigned\narchive flavor while keeping runtime execution validation in place.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Portable packaging currently assumes AXe binaries are signed, which breaks\nwhen consuming AXe's Homebrew-specific unsigned archive. Keep strict\nverification for signed artifacts and fall back to runtime execution\nvalidation for unsigned artifacts.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Stop defaulting AXE_LOCAL_DIR to a user-specific filesystem path.\nLocal AXe bundling now requires explicit AXE_LOCAL_DIR when\nAXE_USE_LOCAL=1, preventing accidental machine-coupled behavior.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Run Homebrew tap updates only for tag-push release runs and push formula\nupdates directly to the tap default branch after release checks pass.\nManual workflow_dispatch runs now avoid Homebrew deployment entirely,\nmatching npm dry-run behavior.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Make portable verification architecture-aware by checking the packaged\nnode-runtime architecture before running wrapper binaries. When host and\nartifact architectures do not match, keep structural validation and skip\nexecution checks to avoid cross-arch runner failures.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Restructure README and GETTING_STARTED installation sections into two clear paths (Homebrew vs npm/npx) with a single-package intro. Replace all stale @beta references with @latest to match the 2.0.0 stable release. Add Homebrew substitution note before client-specific config sections. Document AXe local bundling and release workflow modes. Co-Authored-By: Claude <noreply@anthropic.com>
Remove Smithery from build scripts, release workflow, package metadata,\nand source entrypoints.\n\nDelete Smithery-specific docs and update remaining docs and tests to\nreflect the supported distribution paths without Smithery artifacts. Co-Authored-By: Claude <noreply@anthropic.com>
Remove the CI step that runs the deleted verify:smithery-bundle script\nand rename the build step label to match current packaging flow.\n\nInclude the .gitignore update with this CI cleanup to keep the branch\nstate consistent. Co-Authored-By: Claude <noreply@anthropic.com>
WalkthroughThis pull request removes Smithery-based deployment and replaces it with Homebrew distribution and direct portable packaging. The GitHub Actions release workflow is restructured to build and package macOS binaries (arm64 and universal), publish portable assets to GitHub Releases, and update the Homebrew tap. Documentation is updated to reflect dual installation methods (Homebrew and npm). Supporting infrastructure includes new scripts for macOS portable builds, verification, and Homebrew formula generation. A new resource-root module centralises management of bundled asset paths. Version is bumped to 2.0.0, and all Smithery-related configurations, documentation, and entrypoints are removed. 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In @.github/workflows/release.yml:
- Around line 91-97: The dry-run block sets NPM_TAG to "next" for any VERSION
containing a hyphen, which diverges from the production publish logic that
checks for explicit pre-release tags like "beta", "alpha", and "rc"; update the
dry-run logic so it uses the same tag resolution as production (i.e., inspect
VERSION for specific prerelease identifiers and set NPM_TAG to "beta", "alpha",
"rc", or "latest" accordingly) or factor the tag computation into a shared
step/output (compute NPM_TAG once from VERSION and reuse it for both npm publish
--dry-run and the production npm publish) so both paths are consistent.
In `@scripts/bundle-axe.sh`:
- Line 34: The script uses rm -r for cleanup which can fail on read-only files;
update both removal calls to force removal by using rm -rf instead of rm -r for
the "$BUNDLED_DIR" deletion and the later temp-directory deletion (the second rm
-r near line 235), so change those invocations to use the -f flag to ensure
cleanup doesn't prompt or error under set -e.
In `@scripts/package-macos-portable.sh`:
- Around line 222-235: The .sha256 file currently contains "hash
/full/path/to/file" because create_tarball_and_checksum uses shasum directly;
change the shasum invocation in create_tarball_and_checksum to write only the
hex hash (e.g. pipe shasum -a 256 "$tarball_path" through awk '{print $1}' or
cut to extract the first field) and redirect that single-field hash into
"$checksum_path" so the checksum file contains a bare, path-relative hash
without leaking CI paths.
- Around line 136-173: The install_node_runtime_for_arch function currently
downloads node-v${node_version}-darwin-${node_arch}.tar.gz without integrity
checks; update it to fetch the corresponding SHASUMS256.txt (and optionally
SHASUMS256.txt.sig) from https://nodejs.org/dist/v${node_version}/, extract the
SHA-256 line that matches ${archive_name}, compute the downloaded file's
sha256sum (using shasum -a 256 or similar) and compare values before running tar
-xzf; if the checksums do not match, delete temp_dir and exit non‑zero. Also add
an optional GPG verification step using gpg --verify on SHASUMS256.txt.sig
against the NodeJS release key before trusting the checksum, and ensure all temp
files (archive, SHASUMS256.txt, signature) are cleaned up on both success and
failure.
- Around line 28-52: The argument parsing currently does "${2:-}" and "shift 2"
for value-taking flags (handled in the while/case block) which will fail with a
cryptic error if the flag is last; update each value-taking case for --arch,
--arm64-root, --x64-root, --dist-dir, and --version to first verify that a
non-empty next argument exists and does not start with "-" (i.e., is not another
flag), and if missing print a clear error like "missing value for --arch" (or
the appropriate flag name) and exit non-zero; only then assign the next value to
ARCH, ARM64_ROOT, X64_ROOT, DIST_DIR, or VERSION and perform "shift 2".
In `@scripts/verify-portable-install.sh`:
- Around line 44-60: The trap for cleanup is registered too late causing
TEMP_DIR to leak on early exits; after creating TEMP_DIR with mktemp in the
ARCHIVE_PATH branch (where TEMP_DIR is assigned), register the trap immediately
(trap cleanup EXIT) so cleanup runs on any exit, and ensure the cleanup()
function is defined or moved above that point; update the ARCHIVE_PATH block
around TEMP_DIR, mktemp, tar extraction, extracted_count checks and
PORTABLE_ROOT so the trap is set right after TEMP_DIR creation.
🧹 Nitpick comments (12)
docs/dev/PROJECT_CONFIG_PLAN.md (1)
120-122: Minor grammar nit in the description.Consider adding "the" before "highest precedence" for readability.
Suggested tweak
### 7) Runtime overrides **File:** runtime entrypoints -- Pass overrides into bootstrap/config store, so explicit runtime overrides have highest precedence. +- Pass overrides into bootstrap/config store, so explicit runtime overrides have the highest precedence.scripts/check-docs-cli-commands.js (1)
84-125: Tilde-style fences (~~~) are not recognised.Markdown also supports
~~~as a code fence delimiter. The currentfenceHeaderRegexonly matches triple backticks, so commands inside~~~bashblocks would be picked up as inline code candidates instead, potentially producing false positives. If the docs only use backtick fences this is a non-issue today, but worth noting.Proposed fix
- const fenceHeaderRegex = /^\s*```([a-z0-9_-]*)\s*$/iu; + const fenceHeaderRegex = /^\s*(?:```|~~~)([a-z0-9_-]*)\s*$/iu;scripts/create-homebrew-formula.sh (1)
18-50:shift 2will abort if a flag is passed without a value.When
set -euo pipefailis active, passing e.g.--versionas the very last argument (no value) causesshift 2to fail and the script to exit silently with a non-zero code before the friendly validation message on line 52 is reached. This is acceptable fail-fast behaviour, but a small guard would give a clearer error.Example guard
--version) + [[ $# -ge 2 ]] || { echo "Error: --version requires a value"; usage; exit 1; } VERSION="${2:-}" shift 2 ;;scripts/verify-portable-install.sh (1)
107-110: Host-architecture normalisation is a no-op.
NORMALIZED_HOST_ARCHis set to$HOST_ARCHunconditionally, and theifon line 108 only reassigns the same value (x86_64 → x86_64). If this was meant to mapaarch64→arm64(for potential Linux cross-use), the mapping is missing; otherwise the block can be removed.Option: add the likely intended mapping or remove
NORMALIZED_HOST_ARCH="$HOST_ARCH" -if [[ "$HOST_ARCH" == "x86_64" ]]; then - NORMALIZED_HOST_ARCH="x86_64" +if [[ "$HOST_ARCH" == "aarch64" ]]; then + NORMALIZED_HOST_ARCH="arm64" fisrc/mcp/tools/ui-automation/__tests__/key_press.test.ts (1)
82-94: Consider extracting the duplicatedmockAxeHelpersobject into a shared fixture.The identical
mockAxeHelpersliteral is repeated in every single test case (~10 times). A single declaration at thedescribeblock level (or a factory function) would eliminate significant duplication and make future message changes a one-line edit.Example refactor
// At the top of the outer `describe` block: const defaultMockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', }, ], isError: true, }), }; // Then in each test, just reference `defaultMockAxeHelpers` // or spread + override for the few tests that need different getAxePath values.This applies equally to the other UI-automation test files (button, gesture, long_press, snapshot_ui, swipe, tap, touch, type_text, key_sequence).
Also applies to: 126-138, 173-185, 217-229, 261-273, 297-309, 328-340, 342-354, 369-381, 408-420, 442-454, 476-488
src/utils/axe-helpers.ts (1)
40-51: Bundled path resolution usesexistsSyncwhile other resolvers useisExecutable.
resolveBundledAxePathchecks existence only (existsSync), whereasresolveAxePathFromConfigandresolveAxePathFromPathboth verify executability (isExecutable). If a bundledaxefile exists but lacks the execute bit (e.g., extracted on a filesystem that strips permissions), this would return a non-executable path, and the caller would get a confusing failure later.This is likely fine in practice since
bundle-axe.shapplieschmod +x, but it's worth considering adding anisExecutablecheck for consistency.scripts/bundle-axe.sh (1)
22-28:AXE_VERSIONvariable is used for both input (pinning) and output (detected version).On line 22
AXE_VERSIONis read from the environment to pin the download version, then on line 225 it's unconditionally overwritten with the detected runtime version. This shadow doesn't break anything (the pinned value has already been consumed intoPINNED_AXE_VERSION), but it could confuse future maintainers or downstream consumers that expectAXE_VERSIONto retain the requested version.Consider using a distinct name such as
DETECTED_AXE_VERSIONon line 225.Also applies to: 225-225
.github/workflows/release.yml (2)
315-317:publish_portable_assetsdoes not directly declarebuild_and_package_macosinneeds.This job downloads the
portable-arm64andportable-x64artifacts produced bybuild_and_package_macos, but only transitively depends on it throughbuild_universal_and_verify. It works today because transitive ordering guarantees the job has finished, but an explicit dependency would make the data-flow clearer and protect against future refactors that might remove the intermediate dependency.Suggested fix
publish_portable_assets: if: github.event_name == 'push' - needs: [release, build_universal_and_verify] + needs: [release, build_and_package_macos, build_universal_and_verify] runs-on: ubuntu-latest
396-426: Homebrew tap update pushes directly to the default branch — consider a PR-based flow.Pushing commits directly to the tap's default branch bypasses any branch-protection rules on the tap repository and removes the opportunity for review. A PR-based approach (create a branch, push, open a PR via
gh pr create) would be safer and is the standard pattern used by other Homebrew auto-update bots.Also, the
git cloneon Line 403 embeds the token in the URL, which can surface in CI logs ifset -xor debug mode is enabled. Usinggit -c http.extraheader="AUTHORIZATION: basic $(echo -n x-access-token:${GH_TOKEN} | base64)" clone ...or thegh repo clonehelper avoids this.scripts/package-macos-portable.sh (1)
175-220: Symlink-resolution boilerplate is duplicated across generated wrapper scripts.The
xcodebuildmcpandxcodebuildmcp-doctorwrappers inbin/share identical symlink-resolution and environment-setup logic (Lines 190–199 vs 206–215). If a third wrapper is ever added, or a bug is fixed in the resolution logic, the duplication increases maintenance risk.Since these are generated strings, one approach is to emit a shared
_resolve.shhelper intolibexec/and source it from both wrappers.src/core/resource-root.ts (2)
22-42:getPackageRootis called on every resource-path access — consider memoising.
getPackageRoot(and by extensiongetResourceRoot) performs synchronousfs.existsSynctraversals up the directory tree. Every call togetManifestsDir,getBundledAxePath, orgetBundledFrameworksDirre-does this work. For a CLI tool the overhead is negligible, but a one-liner cache (let _packageRoot: string | undefined) would eliminate repeated filesystem walks and make the intent clearer.Optional memoisation sketch
+let _packageRoot: string | undefined; + export function getPackageRoot(): string { + if (_packageRoot !== undefined) return _packageRoot; const candidates: string[] = []; ... - throw new Error('Could not find package root ...'); + // on success + _packageRoot = found; + return _packageRoot; + ... + throw new Error('Could not find package root ...'); }Apply the same pattern to
getResourceRoot.
44-58:process.execPathis never falsy in Node.js.
process.execPathalways returns the absolute path to the Node binary; the!execPathguard on Line 46 will never be true in any standard Node environment. This is harmless (purely dead code), but worth knowing it's there for defensive reasons only.
Align npm tag resolution between dry-run and production publish, enforce\nrelease asset ordering before Homebrew tap updates, and harden portable\npackaging scripts with checksum verification and safer argument parsing.\n\nAlso improve docs command fence parsing, memoize resource-root resolution\nwith test reset hooks, and reduce duplicated AXe helper fixtures in key\npress UI automation tests. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
Restore symlink resolution in generated bin wrappers before sourcing resource-root helpers. This preserves correct libexec lookup when xcodebuildmcp is invoked via /opt/homebrew/bin symlinks. Co-Authored-By: Claude <noreply@anthropic.com>
Adds portable macOS distribution support end-to-end: runtime resource resolution, AXe bundling hardening, per-arch and universal packaging scripts, release workflow wiring, and automated Homebrew tap formula updates.
Motivation is deterministic CLI execution without requiring user-managed Node and a release path that can ship arm64/x64/universal artifacts plus Homebrew metadata from CI.
SEA packaging was evaluated but not kept due to instability with current ESM runtime behavior. The implementation uses bundled Node runtime + compiled JS for predictable startup and compatibility.
Additional correction included after isolated branch validation:
arm64/x64) instead of copying host runtime.Validation completed on this branch without publishing to npm:
Risk areas for review:
push tagsandworkflow_dispatchXCODEBUILDMCP_RESOURCE_ROOT,DYLD_FRAMEWORK_PATH) under packaged installsNote
Medium Risk
Moderate risk because it significantly changes CI/release automation (publishing, artifact generation/upload, and optional Homebrew tap pushes), where misconfiguration could break releases without affecting runtime code paths.
Overview
Adds a portable macOS distribution pipeline to releases: builds per-arch (
arm64,x64) portable artifacts, verifies them, produces auniversalvariant from the per-arch outputs, and uploads the resulting tarballs +.sha256files to the GitHub Release.Refactors the release workflow to standardize npm tagging via a dedicated
resolve_npm_tagstep (used for both dry-run and production publishes), exposes the releaseversionas a job output for downstream packaging jobs, and removes Smithery-specific verification/deploy steps.Updates docs to present Homebrew as a first-class install option (with Node optional), refreshes MCP client config examples, and removes Smithery-specific developer documentation; also updates
.gitignoreto stop ignoring.smithery/.Written by Cursor Bugbot for commit 5619bbd. This will update automatically on new commits. Configure here.