Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 5 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions apps/desktop/scripts/release-dmg.sh
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
78 changes: 78 additions & 0 deletions apps/web/src/app/download/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="dark mdt-deck flex flex-col min-h-screen bg-background text-foreground font-sans">
<div aria-hidden style={{ position: "fixed", inset: 0, zIndex: -1, pointerEvents: "none" }}>
<div className="mdt-grid" />
<div className="mdt-noise" />
</div>

<Header />

<main className="flex-1">
<section className="mx-auto max-w-3xl px-6 pt-32 pb-24 text-center">
<p className="mdt-kicker">Desktop app · macOS</p>
<h1 className="mt-4 text-4xl sm:text-5xl font-semibold tracking-tight">
Your entire dev toolkit,{" "}
<span className="mdt-grad-text">native on your Mac.</span>
</h1>
<p className="mt-5 text-lg text-muted-foreground">
The full MyDevTools suite as a signed, notarized macOS app. Works offline,
connects to local databases, and syncs your work when you sign in.
</p>

<div className="mt-10 flex flex-col items-center">
<DownloadDesktopButton />
</div>

{/* Verify your download */}
<div className="mt-16 mx-auto max-w-xl text-left mdt-surface rounded-xl p-6">
<h2 className="text-sm font-semibold">Verify your download (optional)</h2>
<p className="mt-2 text-sm text-muted-foreground">
Each release ships a <code>.sha256</code> checksum on the{" "}
<a
className="underline underline-offset-2"
href="https://github.com/itsmeakhil/mydevtools/releases/latest"
target="_blank"
rel="noreferrer"
>
GitHub release page
</a>
. After downloading, confirm the file is intact:
</p>
<pre className="mt-3 overflow-x-auto rounded-lg bg-black/40 p-3 text-xs text-muted-foreground">
<code>shasum -a 256 ~/Downloads/MyDevTools_*_universal.dmg</code>
</pre>
<p className="mt-3 text-xs text-muted-foreground">
The app is signed with an Apple Developer ID and notarized by Apple, so it
opens with a normal double-click — no security warnings.
</p>
</div>
</section>
</main>

<Footer />
</div>
);
}
85 changes: 85 additions & 0 deletions apps/web/src/components/download-desktop-button.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg viewBox="0 0 384 512" aria-hidden className={className} fill="currentColor">
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
</svg>
);
}

export function DownloadDesktopButton({
size = "lg",
className,
}: {
size?: "default" | "sm" | "lg";
className?: string;
}) {
const [dmgUrl, setDmgUrl] = useState<string | null>(null);
const [version, setVersion] = useState<string | null>(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 (
<div className={className}>
<Button asChild size={size} className="gap-2">
<a href={href} download>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<AppleGlyph className="h-4 w-4" />
)}
Download for macOS
<Download className="h-4 w-4 opacity-70" />
</a>
</Button>
<p className="mt-2 text-sm text-muted-foreground">
{version ? `${version} · ` : ""}Universal (Apple Silicon &amp; Intel) · macOS 12+
</p>
</div>
);
}