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
97 changes: 96 additions & 1 deletion .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ on:
default: 30

jobs:
build:
build-macos:
name: Build - macOS (${{ matrix.arch }})
runs-on: macos-latest
environment: production
Expand Down Expand Up @@ -140,3 +140,98 @@ jobs:
path: apps/desktop/release/*-mac.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

build-linux:
name: Build - Linux (x64)
runs-on: ubuntu-latest
environment: production

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.2"

- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ github.sha }}
restore-keys: |
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen

- name: Set version suffix
if: inputs.version_suffix != ''
working-directory: apps/desktop
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
NEW_VERSION="${CURRENT_VERSION}${{ inputs.version_suffix }}"
echo "Setting version to: $NEW_VERSION"
node -e "
const fs = require('fs');
const pkg = require('./package.json');
pkg.version = '$NEW_VERSION';
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, '\t') + '\n');
"
echo "Updated package.json version to $NEW_VERSION"

- name: Clean dev folder
working-directory: apps/desktop
run: bun run clean:dev

- name: Compile app with electron-vite
working-directory: apps/desktop
env:
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }}
NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }}
NEXT_PUBLIC_ELECTRIC_URL: ${{ secrets.NEXT_PUBLIC_ELECTRIC_URL }}
SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SUPERSET_WORKSPACE_NAME: superset
run: bun run compile:app

- name: Build Electron app
working-directory: apps/desktop
run: bun run package -- --publish never --config ${{ inputs.electron_builder_config }}

- name: Verify Linux AppImage + update manifest exist
working-directory: apps/desktop
run: |
ls -la release
test -n "$(ls -1 release/*.AppImage 2>/dev/null)" || {
echo "::error::No AppImage artifact generated in apps/desktop/release"
exit 1
}
test -n "$(ls -1 release/*-linux.yml 2>/dev/null)" || {
echo "::error::No Linux auto-update manifest generated in apps/desktop/release"
exit 1
}

- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-linux-appimage
path: apps/desktop/release/*.AppImage
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

- name: Upload Linux auto-update manifest
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-linux-update-manifest
path: apps/desktop/release/*-linux.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error
20 changes: 20 additions & 0 deletions .github/workflows/release-desktop-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ jobs:
pattern: desktop-canary-*
merge-multiple: true

- name: Create canary-named copies for updater URLs
run: |
cd release-artifacts
for file in *.AppImage; do
if [[ -f "$file" ]]; then
arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.AppImage$/\1/')
cp "$file" "Superset-Canary-${arch}.AppImage"
echo "Created canary copy: Superset-Canary-${arch}.AppImage"
fi
done
# Prerelease builds may request canary-linux.yml; keep latest-linux.yml as fallback.
for file in *-linux.yml; do
if [[ -f "$file" ]]; then
cp "$file" "canary-linux.yml"
cp "$file" "latest-linux.yml"
echo "Created canary manifests: canary-linux.yml, latest-linux.yml"
break
fi
done

- name: List artifacts
run: |
echo "Release artifacts:"
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ jobs:
echo "Created stable copy: Superset-${arch}-mac.zip"
fi
done
for file in *.AppImage; do
if [[ -f "$file" ]]; then
# Extract architecture from filename (e.g., superset-0.0.1-x64.AppImage -> x64)
arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.AppImage$/\1/')
cp "$file" "Superset-${arch}.AppImage"
echo "Created stable copy: Superset-${arch}.AppImage"
fi
done
# Keep Linux updater manifest at a stable filename for generic provider lookups.
for file in *-linux.yml; do
if [[ -f "$file" && "$file" != "latest-linux.yml" ]]; then
cp "$file" "latest-linux.yml"
echo "Created stable copy: latest-linux.yml"
break
fi
done
echo "Release artifacts:"
ls -la

Expand Down
18 changes: 18 additions & 0 deletions apps/api/src/app/api/auth/desktop/connect/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export async function GET(request: Request) {
const provider = url.searchParams.get("provider");
const state = url.searchParams.get("state");
const protocol = url.searchParams.get("protocol");
const localCallback = url.searchParams.get("local_callback");

if (!provider || !state) {
return new Response("Missing provider or state", { status: 400 });
Expand All @@ -22,6 +23,23 @@ export async function GET(request: Request) {
if (protocol) {
successUrl.searchParams.set("desktop_protocol", protocol);
}
if (localCallback) {
try {
const callbackUrl = new URL(localCallback);
const isLoopback =
callbackUrl.protocol === "http:" &&
(callbackUrl.hostname === "127.0.0.1" ||
callbackUrl.hostname === "localhost");
if (isLoopback && callbackUrl.pathname === "/auth/callback") {
successUrl.searchParams.set(
"desktop_local_callback",
callbackUrl.toString(),
);
}
} catch {
// Ignore invalid callback URLs and continue with deep-link flow.
}
}

const result = await auth.api.signInSocial({
body: {
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@ import { type NextRequest, NextResponse } from "next/server";

import { env } from "./env";

const desktopDevPort = process.env.DESKTOP_VITE_PORT || "5173";
const desktopDevOrigins =
process.env.NODE_ENV === "development"
? [
`http://localhost:${desktopDevPort}`,
`http://127.0.0.1:${desktopDevPort}`,
]
: [];

const allowedOrigins = [
env.NEXT_PUBLIC_WEB_URL,
env.NEXT_PUBLIC_ADMIN_URL,
env.NEXT_PUBLIC_DESKTOP_URL,
...desktopDevOrigins,
].filter(Boolean);

function getCorsHeaders(origin: string | null) {
Expand Down
28 changes: 27 additions & 1 deletion apps/desktop/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,30 @@ This skips environment variable validation and the sign-in screen, useful for lo

# Release

When building for release, make sure node-pty is built for the correct architecture with `bun install:deps` and then run `bun release`
When building for release, make sure `node-pty` is built for the correct architecture with `bun run install:deps`, then run `bun run release`.

# Linux (AppImage) local build

From `apps/desktop`:

```bash
bun run clean:dev
bun run compile:app
bun run package -- --publish never --config electron-builder.ts
```

Expected outputs in `apps/desktop/release/`:

- `*.AppImage`
- `*-linux.yml` (Linux auto-update manifest)

# Linux auto-update verification (local)

From `apps/desktop` after packaging:

```bash
ls -la release/*.AppImage
ls -la release/*-linux.yml
```

If both files exist, packaging produced the Linux artifact + updater metadata that `electron-updater` expects.
14 changes: 11 additions & 3 deletions apps/desktop/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ This creates a draft release. Publish it manually at GitHub Releases.

The app checks for updates at launch and every x hours using:

- **Manifest**: `https://github.com/superset-sh/superset/releases/latest/download/latest-mac.yml`
- **Installer**: `https://github.com/superset-sh/superset/releases/latest/download/Superset-arm64.dmg`
- **macOS manifest**: `https://github.com/superset-sh/superset/releases/latest/download/latest-mac.yml`
- **Linux manifest**: `https://github.com/superset-sh/superset/releases/latest/download/latest-linux.yml`
- **macOS installer**: `https://github.com/superset-sh/superset/releases/latest/download/Superset-arm64.dmg`
- **Linux installer**: `https://github.com/superset-sh/superset/releases/latest/download/Superset-x64.AppImage`

The workflow creates stable-named copies (without version) so these URLs always point to the latest build.

Expand All @@ -78,7 +80,13 @@ bun run package

Output: `apps/desktop/release/`

Linux output should include:

- `*.AppImage`
- `*-linux.yml` (auto-update manifest)

## Troubleshooting

- **Build fails**: Check `src/resources/build/icons/icon.icns` exists
- **Linux auto-update not working**: Verify `release/*-linux.yml` is uploaded to the GitHub release
- **Build icon warnings/failures**: Add icons under `src/resources/build/icons/` (`icon.icns`, `icon.ico`, optional Linux `.png`)
- **Native module errors**: Ensure `node-pty` is in externals in both `electron.vite.config.ts` and `electron-builder.ts`
13 changes: 12 additions & 1 deletion apps/desktop/create-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ if ! command -v gh &> /dev/null; then
error "GitHub CLI (gh) is required but not installed.\nInstall it from: https://cli.github.com/"
fi

# Check if jq is installed (required for package.json version updates)
if ! command -v jq &> /dev/null; then
error "jq is required but not installed.\nInstall it with your package manager (e.g. sudo apt install jq)"
fi

# Check if authenticated with gh
if ! gh auth status &> /dev/null; then
error "Not authenticated with GitHub CLI.\nRun: gh auth login"
Expand Down Expand Up @@ -333,6 +338,7 @@ REPO=$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')
# 6. Monitor the workflow
info "Monitoring GitHub Actions workflow..."
echo " Waiting for workflow to start (this may take a few seconds)..."
TAG_SHA=$(git rev-list -n 1 "${TAG_NAME}")

# Wait and retry to find the workflow run
MAX_RETRIES=6
Expand All @@ -341,7 +347,11 @@ WORKFLOW_RUN=""

while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ -z "$WORKFLOW_RUN" ]; do
sleep 5
WORKFLOW_RUN=$(gh run list --workflow=release-desktop.yml --json databaseId,headBranch,status --jq ".[] | select(.headBranch == \"${TAG_NAME}\") | .databaseId" | head -1)
WORKFLOW_RUN=$(gh run list \
--workflow=release-desktop.yml \
--json databaseId,headSha,event,createdAt \
--jq ".[] | select(.headSha == \"${TAG_SHA}\" and .event == \"push\") | .databaseId" \
| head -1)
RETRY_COUNT=$((RETRY_COUNT + 1))

if [ -z "$WORKFLOW_RUN" ] && [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
Expand Down Expand Up @@ -430,6 +440,7 @@ else
echo ""
echo -e "${BLUE}Direct download:${NC}"
echo " • ${LATEST_URL}/download/Superset-arm64.dmg"
echo " • ${LATEST_URL}/download/Superset-x64.AppImage"
echo ""
else
success "Draft release created!"
Expand Down
10 changes: 7 additions & 3 deletions apps/desktop/electron-builder.canary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
* @see https://www.electron.build/configuration/configuration
*/

import { existsSync } from "node:fs";
import { join } from "node:path";
import type { Configuration } from "electron-builder";
import baseConfig from "./electron-builder";
import pkg from "./package.json";

const productName = "Superset Canary";
const canaryMacIconPath = join(pkg.resources, "build/icons/icon-canary.icns");
const canaryLinuxIconPath = join(pkg.resources, "build/icons/icon-canary.png");
const canaryWinIconPath = join(pkg.resources, "build/icons/icon-canary.ico");

const config: Configuration = {
...baseConfig,
Expand All @@ -28,7 +32,7 @@ const config: Configuration = {

mac: {
...baseConfig.mac,
icon: join(pkg.resources, "build/icons/icon-canary.icns"),
...(existsSync(canaryMacIconPath) ? { icon: canaryMacIconPath } : {}),
artifactName: `Superset-Canary-\${version}-\${arch}.\${ext}`,
extendInfo: {
CFBundleName: productName,
Expand All @@ -38,14 +42,14 @@ const config: Configuration = {

linux: {
...baseConfig.linux,
icon: join(pkg.resources, "build/icons/icon-canary.png"),
...(existsSync(canaryLinuxIconPath) ? { icon: canaryLinuxIconPath } : {}),
synopsis: `${pkg.description} (Canary)`,
artifactName: `superset-canary-\${version}-\${arch}.\${ext}`,
},

win: {
...baseConfig.win,
icon: join(pkg.resources, "build/icons/icon-canary.ico"),
...(existsSync(canaryWinIconPath) ? { icon: canaryWinIconPath } : {}),
artifactName: `Superset-Canary-\${version}-\${arch}.\${ext}`,
},
};
Expand Down
Loading
Loading