From 099217492e61516dee635496e5bfb7caefcb2023 Mon Sep 17 00:00:00 2001 From: Nishanth Date: Wed, 15 Jul 2026 10:19:49 +0530 Subject: [PATCH] desktop: wire GitHub-Releases auto-update + macOS download button (v0.1.0) Set up the desktop release + auto-update pipeline on GitHub Releases: - updater endpoint -> releases/latest/download/latest.json, and rotate to a fresh minisign pubkey (B0A29640) whose private key is held for CI signing - release.yml: auto-publish (releaseDraft: false) and trim the build matrix to macOS-only to conserve the org's free Actions minutes (macOS bills at 10x) - add apps/web /download page + DownloadDesktopButton (reads the latest GitHub release via API, links straight to the universal .dmg, stays current across releases) - add apps/desktop/scripts/release-dmg.sh for local signed+notarized DMG builds Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 29 ++----- apps/desktop/scripts/release-dmg.sh | 73 ++++++++++++++++ apps/desktop/src-tauri/tauri.conf.json | 4 +- apps/web/src/app/download/page.tsx | 78 +++++++++++++++++ .../components/download-desktop-button.tsx | 85 +++++++++++++++++++ 5 files changed, 243 insertions(+), 26 deletions(-) create mode 100755 apps/desktop/scripts/release-dmg.sh create mode 100644 apps/web/src/app/download/page.tsx create mode 100644 apps/web/src/components/download-desktop-button.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2e32ab7..ec600340 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,21 +13,14 @@ jobs: fail-fast: false matrix: include: - # macOS universal (Intel + Apple Silicon) + # macOS universal (Intel + Apple Silicon). + # macOS-only for now — Windows/Linux runners were dropped to conserve + # the org's free Actions minutes (macOS bills at 10x). Re-add matrix + # entries here to build those platforms again. - platform: macos-latest args: '--target universal-apple-darwin' rust_target: 'aarch64-apple-darwin,x86_64-apple-darwin' - # Windows x64 - - platform: windows-latest - args: '' - rust_target: '' - - # Linux x64 - - platform: ubuntu-22.04 - args: '' - rust_target: '' - runs-on: ${{ matrix.platform }} steps: @@ -54,18 +47,6 @@ jobs: with: workspaces: apps/desktop/src-tauri -> target - - name: Install Linux system dependencies - if: matrix.platform == 'ubuntu-22.04' - run: | - sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf \ - libssl-dev \ - pkg-config - - name: Install frontend dependencies run: pnpm install @@ -97,7 +78,7 @@ jobs: releaseName: MyDevTools ${{ github.ref_name }} releaseBody: | See [CHANGELOG](https://github.com/itsmeakhil/mydevtools/blob/main/CHANGELOG.md) for details. - releaseDraft: true + releaseDraft: false prerelease: false args: ${{ matrix.args }} updaterJsonPath: latest.json diff --git a/apps/desktop/scripts/release-dmg.sh b/apps/desktop/scripts/release-dmg.sh new file mode 100755 index 00000000..cc4f1daf --- /dev/null +++ b/apps/desktop/scripts/release-dmg.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# One-shot signed + notarized universal macOS DMG for distribution. +# +# Does the full release flow that `tauri build`'s own dmg step can't do reliably +# (its bundle_dmg.sh drives Finder via AppleScript and flakes headless): +# 1. tauri build (universal) — compiles & signs the .app (hardened runtime) +# 2. notarizes & staples the .app itself (so it works offline) +# 3. wraps the stapled .app into a DMG via hdiutil (deterministic, no Finder), +# then signs, notarizes & staples the DMG +# 4. verifies with Gatekeeper and emits a .sha256 checksum +# +# Signing uses your login keychain, so RUN THIS IN AN INTERACTIVE TERMINAL +# (codesign needs to reach your private key; a detached/non-interactive shell +# hits errSecInternalComponent or hangs on a keychain prompt). +# +# Notarization uses a stored keychain profile so no password is needed inline. +# Create it once: +# xcrun notarytool store-credentials "mydevtools-notary" \ +# --apple-id "you@email.com" --team-id "STTF2NVQK8" --password "app-specific-pw" +# +# Env: +# APPLE_SIGNING_IDENTITY (optional) override; defaults to the value below +# NOTARY_PROFILE (optional) keychain profile name; defaults to mydevtools-notary +# Firebase/back-end vars are read from apps/web/.env.local by build-tauri.mjs. +# +# Usage: bash scripts/release-dmg.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +VERSION="$(node -p "require('$ROOT/src-tauri/tauri.conf.json').version")" +BUNDLE="$ROOT/src-tauri/target/universal-apple-darwin/release/bundle" +APP="$BUNDLE/macos/MyDevTools.app" +DMG="$BUNDLE/dmg/MyDevTools_${VERSION}_universal.dmg" + +: "${APPLE_SIGNING_IDENTITY:=Developer ID Application: Nishanth P V (STTF2NVQK8)}" +: "${NOTARY_PROFILE:=mydevtools-notary}" +export APPLE_SIGNING_IDENTITY + +echo "▸ [1/5] Building & signing the app (universal)…" +# Build only the .app; the DMG we assemble ourselves below. Tauri signs the app +# with APPLE_SIGNING_IDENTITY (hardened runtime). No APPLE_ID/PASSWORD in env, so +# Tauri does NOT notarize here — we do that explicitly in step 2. +( cd "$ROOT" && pnpm tauri build --target universal-apple-darwin --bundles app ) + +echo "▸ [2/5] Notarizing & stapling the app…" +# notarytool needs an archive, not a bare .app — zip it, submit, then staple the +# ticket back onto the .app so it launches offline without a Gatekeeper round-trip. +APP_ZIP="$(mktemp -d)/MyDevTools.zip" +ditto -c -k --keepParent "$APP" "$APP_ZIP" +xcrun notarytool submit "$APP_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait +xcrun stapler staple "$APP" +rm -f "$APP_ZIP" + +echo "▸ [3/5] Wrapping the notarized app into a DMG (hdiutil)…" +mkdir -p "$BUNDLE/dmg" +STAGE="$(mktemp -d)" +cp -R "$APP" "$STAGE/" +ln -s /Applications "$STAGE/Applications" +rm -f "$DMG" +hdiutil create -volname "MyDevTools" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null +rm -rf "$STAGE" +codesign --force --sign "$APPLE_SIGNING_IDENTITY" "$DMG" + +echo "▸ [4/5] Notarizing & stapling the DMG…" +xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait +xcrun stapler staple "$DMG" + +echo "▸ [5/5] Verifying + checksum…" +spctl -a -vvv -t open --context context:primary-signature "$DMG" +shasum -a 256 "$DMG" | tee "$DMG.sha256" + +echo "" +echo "✅ Done: $DMG" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 2576b15e..57d3d237 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -31,8 +31,8 @@ } }, "updater": { - "endpoints": ["https://releases.mydevtools.tech/latest.json"], - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQ5RjY1NDFDNTgxQUM0MDgKUldRSXhCcFlIRlQyMlVkbGlkZ1ozYmFQR1REYU8yWWNqZWhJcUdqK0ZRb3VHeWR6NHFRZzRTTmoK" + "endpoints": ["https://github.com/itsmeakhil/mydevtools/releases/latest/download/latest.json"], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIwQTI5NjQwRjg5MjgwNjQKUldSa2dKTDRRSmFpc0Rqc21XWGdNSGVPNlp4YVlVV0JWdFpJVk9kYzBIdkx2MUFHNHZWc1VKUmsK" } }, "bundle": { diff --git a/apps/web/src/app/download/page.tsx b/apps/web/src/app/download/page.tsx new file mode 100644 index 00000000..ec4ce09f --- /dev/null +++ b/apps/web/src/app/download/page.tsx @@ -0,0 +1,78 @@ +import type { Metadata } from "next"; + +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { DownloadDesktopButton } from "@/components/download-desktop-button"; + +const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://mydevtools.tech"; + +export const metadata: Metadata = { + title: "Download MyDevTools for macOS", + description: + "Download the MyDevTools desktop app for macOS — your entire dev toolkit, native. Universal build for Apple Silicon and Intel, signed and notarized by Apple.", + alternates: { canonical: `${baseUrl}/download` }, + openGraph: { + title: "Download MyDevTools for macOS | MyDevTools", + description: "Native macOS desktop app — signed & notarized. Apple Silicon and Intel.", + url: `${baseUrl}/download`, + siteName: "MyDevTools", + type: "website", + }, +}; + +export default function DownloadPage() { + return ( +
+
+
+
+
+ +
+ +
+
+

Desktop app · macOS

+

+ Your entire dev toolkit,{" "} + native on your Mac. +

+

+ The full MyDevTools suite as a signed, notarized macOS app. Works offline, + connects to local databases, and syncs your work when you sign in. +

+ +
+ +
+ + {/* Verify your download */} +
+

Verify your download (optional)

+

+ Each release ships a .sha256 checksum on the{" "} + + GitHub release page + + . After downloading, confirm the file is intact: +

+
+              shasum -a 256 ~/Downloads/MyDevTools_*_universal.dmg
+            
+

+ The app is signed with an Apple Developer ID and notarized by Apple, so it + opens with a normal double-click — no security warnings. +

+
+
+
+ +
+
+ ); +} diff --git a/apps/web/src/components/download-desktop-button.tsx b/apps/web/src/components/download-desktop-button.tsx new file mode 100644 index 00000000..12427216 --- /dev/null +++ b/apps/web/src/components/download-desktop-button.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Download, Loader2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +/** + * "Download for macOS" button. Reads the latest published GitHub release via the + * API and links straight to its universal .dmg asset, so the link stays current + * across releases without editing the site. Falls back to the releases page if + * the API is unreachable or no asset is found yet. + */ +const REPO = "itsmeakhil/mydevtools"; +const RELEASES_PAGE = `https://github.com/${REPO}/releases/latest`; + +type Asset = { name: string; browser_download_url: string }; + +/** Minimal Apple logo (lucide has no Apple icon in this version). */ +function AppleGlyph({ className }: { className?: string }) { + return ( + + + + ); +} + +export function DownloadDesktopButton({ + size = "lg", + className, +}: { + size?: "default" | "sm" | "lg"; + className?: string; +}) { + const [dmgUrl, setDmgUrl] = useState(null); + const [version, setVersion] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { + headers: { Accept: "application/vnd.github+json" }, + }) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status))))) + .then((d) => { + if (!active) return; + const assets: Asset[] = Array.isArray(d.assets) ? d.assets : []; + const dmg = + assets.find((a) => /universal.*\.dmg$/i.test(a.name)) || + assets.find((a) => a.name.toLowerCase().endsWith(".dmg")); + setDmgUrl(dmg?.browser_download_url ?? null); + setVersion(typeof d.tag_name === "string" ? d.tag_name : null); + }) + .catch(() => { + /* offline / rate-limited / no release yet — fall back to releases page */ + }) + .finally(() => { + if (active) setLoading(false); + }); + return () => { + active = false; + }; + }, []); + + const href = dmgUrl ?? RELEASES_PAGE; + + return ( +
+ +

+ {version ? `${version} · ` : ""}Universal (Apple Silicon & Intel) · macOS 12+ +

+
+ ); +}