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
28 changes: 28 additions & 0 deletions .github/release-notes/desktop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Grida Desktop — Technical Preview

This is a nightly release of Grida `{{version}}`.

- macOS (signed, notarized)
- darwin arm64 — Apple Silicon
- dmg · zip
- linux
- deb · rpm
- arm64 · x64
- windows (not signed)
- x64

[Join Slack](https://grida.co/join-slack) to learn more.

## Supported builds

| Name | Platform | x64 | arm64 | makers | signed | notes |
| ------- | -------- | --- | ----- | ---------------- | ------ | ----------------------------------------------------------------- |
| `Grida` | `darwin` | | ✓ | `zip`, `dmg` | ✓ | Apple Silicon only. Rosetta 2 cannot run arm64 binaries on Intel. |
| `Grida` | `win32` | ✓ | | `exe (squirrel)` | | x64 only / not signed |
| `Grida` | `linux` | ✓ | ✓ | `deb`, `rpm` | | |

## Auto-update

macOS installs receive updates automatically via [`update.electronjs.org`](https://github.com/electron/update.electronjs.org). The running app polls every 6 hours; when a newer release is available, a "Restart to update" prompt appears.

Releases marked **pre-release** on this page are _not_ served to existing installs — they're for manual download / testing only.
15 changes: 15 additions & 0 deletions .github/workflows/realease-desktop-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,18 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: pnpm run publish:prerelease --arch="arm64"

- name: Update release notes from template
# Runs once (gated to the linux row, which has no signing-secret
# dependency and therefore always runs). The release tag will exist
# by this point since linux's publish step ran just above.
# Template at .github/release-notes/desktop.md.
if: ${{ matrix.os.name == 'linux' }}
working-directory: ${{ github.workspace }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=$(node -p "require('./desktop/package.json').version")
TAG="v${VERSION}"
BODY=$(sed "s|{{version}}|${VERSION}|g" .github/release-notes/desktop.md)
gh release edit "$TAG" --notes "$BODY" --repo gridaco/grida
121 changes: 97 additions & 24 deletions editor/app/(www)/(downloads)/downloads/downloads.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
import { unstable_cache } from "next/cache";
import assert from "assert";

export namespace downloads {
Expand All @@ -7,9 +8,7 @@ export namespace downloads {
type GithubReleaseAsset = GithubReleaseAssets[number];

export interface DownloadLinks {
mac_dmg_x64: string;
mac_dmg_arm64: string;
mac_dmg_universal: string;
linux_deb_x64: string;
linux_rpm_x64: string;
linux_deb_arm64: string;
Expand All @@ -18,7 +17,7 @@ export namespace downloads {
}

export type Platform = "mac" | "windows" | "linux";
export type Arch = "x64" | "arm64" | "universal";
export type Arch = "x64" | "arm64";
export type Maker = "dmg" | "squirrel.windows" | "deb" | "rpm";

type Distro = {
Expand All @@ -27,7 +26,6 @@ export namespace downloads {
ext: "dmg" | "exe" | "deb" | "rpm";
arch: {
arm64?: string | undefined;
universal?: string | undefined;
x64?: string | undefined;
};
};
Expand All @@ -45,8 +43,6 @@ export namespace downloads {
ext: "dmg",
arch: {
arm64: "arm64",
universal: "universal",
x64: "x64",
},
},
],
Expand Down Expand Up @@ -97,19 +93,15 @@ export namespace downloads {
export async function getLinks(): Promise<DownloadLinks> {
const f = new Fetcher();

const mac_dmg_x64 = await f.getAsset("mac", "dmg", "x64");
const mac_dmg_arm64 = await f.getAsset("mac", "dmg", "arm64");
const mac_dmg_universal = await f.getAsset("mac", "dmg", "universal");
const linux_deb_x64 = await f.getAsset("linux", "deb", "x64");
const linux_rpm_x64 = await f.getAsset("linux", "rpm", "x64");
const linux_deb_arm64 = await f.getAsset("linux", "deb", "arm64");
const linux_rpm_arm64 = await f.getAsset("linux", "rpm", "arm64");
const windows_x64 = await f.getAsset("windows", "squirrel.windows", "x64");

return {
mac_dmg_x64: mac_dmg_x64.browser_download_url,
mac_dmg_arm64: mac_dmg_arm64.browser_download_url,
mac_dmg_universal: mac_dmg_universal.browser_download_url,
linux_deb_x64: linux_deb_x64.browser_download_url,
linux_rpm_x64: linux_rpm_x64.browser_download_url,
linux_deb_arm64: linux_deb_arm64.browser_download_url,
Expand All @@ -118,9 +110,82 @@ export namespace downloads {
};
}

export interface DownloadLinksForPage extends DownloadLinks {
default: {
platform: Platform;
maker: Maker;
arch: Arch;
url: string;
} | null;
}

function pickDefault(
os: Platform | null,
links: DownloadLinks
): DownloadLinksForPage["default"] {
switch (os) {
case "mac":
return {
platform: "mac",
maker: "dmg",
arch: "arm64",
url: links.mac_dmg_arm64,
};
case "windows":
return {
platform: "windows",
maker: "squirrel.windows",
arch: "x64",
url: links.windows_exe_x64,
};
case "linux":
return {
platform: "linux",
maker: "deb",
arch: "x64",
url: links.linux_deb_x64,
};
default:
return null;
}
}

// Cached wrapper around getLinks() — hits GitHub's unauthenticated
// releases endpoint at most ~once per hour per region, keeping the
// page out of trouble with the 60 req/hr/IP rate limit.
// `unstable_cache` only memoizes successful resolutions; on throw,
// the next render re-attempts (so transient API blips self-heal).
const getCachedLinks = unstable_cache(
() => getLinks(),
["downloads:getLinks"],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
revalidate: 3600,
}
);

/**
* Page-bound helper: latest-release links + OS-aware default, cached.
* Falls back to the static v0.0.1 URLs if the GitHub API is unreachable.
*/
export async function getLinksForPage(
os: Platform | null
): Promise<DownloadLinksForPage> {
try {
const links = await getCachedLinks();
return { ...links, default: pickDefault(os, links) };
} catch (err) {
console.warn(
"[downloads] getLinks failed; falling back to static v0.0.1 links.",
err
);
return getLinks_v001(os);
}
}

/**
* @deprecated
* temporary static version of getLinks, until we find a reliable way to fetch release without rate limiting
* Static fallback used only when the GitHub API is unreachable.
* Prefer `getLinksForPage()`, which calls this internally on failure.
*/
export function getLinks_v001(
platform: Platform | null,
Expand All @@ -134,12 +199,8 @@ export namespace downloads {
} | null;
} {
const links: DownloadLinks = {
mac_dmg_x64:
"https://github.com/gridaco/grida/releases/download/v0.0.1/Grida-0.0.1-x64.dmg",
mac_dmg_arm64:
"https://github.com/gridaco/grida/releases/download/v0.0.1/Grida-0.0.1-arm64.dmg",
mac_dmg_universal:
"https://github.com/gridaco/grida/releases/download/v0.0.1/Grida-0.0.1-universal.dmg",
linux_deb_x64:
"https://github.com/gridaco/grida/releases/download/v0.0.1/grida_0.0.1_amd64.deb",
linux_rpm_x64:
Expand All @@ -163,8 +224,8 @@ export namespace downloads {
d = {
platform: "mac",
maker: "dmg",
arch: "universal",
url: links.mac_dmg_universal,
arch: "arm64",
url: links.mac_dmg_arm64,
};
break;
}
Expand All @@ -189,12 +250,8 @@ export namespace downloads {

return {
default: d,
mac_dmg_x64:
"https://github.com/gridaco/grida/releases/download/v0.0.1/Grida-0.0.1-x64.dmg",
mac_dmg_arm64:
"https://github.com/gridaco/grida/releases/download/v0.0.1/Grida-0.0.1-arm64.dmg",
mac_dmg_universal:
"https://github.com/gridaco/grida/releases/download/v0.0.1/Grida-0.0.1-universal.dmg",
linux_deb_x64:
"https://github.com/gridaco/grida/releases/download/v0.0.1/grida_0.0.1_amd64.deb",
linux_rpm_x64:
Expand Down Expand Up @@ -230,9 +287,25 @@ export namespace downloads {
}

async fetch() {
const release = await fetchrelease();
this.m_tag = release.data.tag_name;
this.m_assets = release.data.assets;
// Memoize: subsequent calls return the same release without re-hitting
// GitHub. `getLinks()` resolves 6 assets via separate `getAsset` calls;
// without this, a single cache miss in `getCachedLinks` would burn
// 6 unauthenticated requests against the 60/hr/IP limit.
//
// Failures are memoized as `[]` so the same cap applies on the error
// path — otherwise `m_assets` stays null and each of the 6 sequential
// getAsset calls retries the API. `getAsset` already handles the
// empty-asset case gracefully via its assert+catch, returning null.
// A fresh Fetcher is created per render, so the error cache is
// per-render and transient failures self-heal on the next request.
if (this.m_assets) return this.m_assets;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
try {
const release = await fetchrelease();
this.m_tag = release.data.tag_name;
this.m_assets = release.data.assets;
} catch {
this.m_assets = [];
}
return this.m_assets;
}

Expand Down
16 changes: 1 addition & 15 deletions editor/app/(www)/(downloads)/downloads/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function DownloadsPage() {
const userAgent = headersList.get("user-agent");

const os = downloads.getDesktopOS(userAgent || "");
const links = await downloads.getLinks_v001(os);
const links = await downloads.getLinksForPage(os);

return (
<main className="min-h-screen flex flex-col">
Expand Down Expand Up @@ -96,27 +96,13 @@ export default async function DownloadsPage() {
function DownloadButtons({ links }: { links: downloads.DownloadLinks }) {
return (
<div className="container max-w-3xl flex flex-wrap justify-center gap-3 mx-auto">
{/* macOS Universal */}
<Link href={links.mac_dmg_universal} download>
<Button size="lg" variant="outline">
<Apple className="size-4" /> Download for macOS (Universal)
</Button>
</Link>

{/* macOS Apple Silicon */}
<Link href={links.mac_dmg_arm64} download>
<Button size="lg" variant="outline">
<Apple className="size-4" /> Download for macOS (Apple Silicon)
</Button>
</Link>

{/* macOS Intel */}
<Link href={links.mac_dmg_x64} download>
<Button size="lg" variant="outline">
<Apple className="size-4" /> Download for macOS (Intel-based Macs)
</Button>
</Link>

{/* Windows x64 */}
<Link href={links.windows_exe_x64} download>
<Button size="lg" variant="outline">
Expand Down
Loading