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 (
+
+ );
+}