mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
## Summary
The RDE dashboard was bundled into the `@hexclave/cli` npm tarball
(~165MB). That made the `npx @latest` auto-update re-exec pull a huge
tarball, which tripped download firewalls (Socket Firewall
false-positives on Replit → `Lock compromised`).
This decouples the dashboard from the CLI: it's published as a **GitHub
Release artifact** (a zip + `manifest.json` on a floating
`dashboard-latest` release) and fetched at runtime, cached on disk. The
dashboard now rolls forward **independently of the CLI version**
(assuming newest dashboards stay compatible with older CLIs — no compat
gate yet, by design for now).
## How it works
- **Runtime** (`packages/cli/src/lib/dashboard-release.ts`): on
`hexclave dev`, fetch `manifest.json` (`{version, sha256, url}`) from a
stable, no-API GitHub URL → cache under
`<dirname(devEnvStatePath())>/dashboards/<version>/` → streamed
download, **sha256 verify**, extract via the existing `extract-zip` dep,
atomic publish. Offline → fall back to the newest cached build.
- **Wiring** (`dev.ts`): `startDashboardIfNeeded` resolves/launches the
cached release; the restart decision now compares the **dashboard
release** version (not the CLI version). `HEXCLAVE_DASHBOARD_DIR` runs a
local build with zero network; a custom dev dashboard command bypasses
releases entirely.
- **No more npx self-update**: the `npx @latest` re-exec in `hexclave
dev` is removed entirely (along with `self-update.ts`, the
`--no-auto-update` flag, and the binName plumbing that only built the
npx call). It existed to keep the bundled dashboard fresh; now the
dashboard self-updates from GitHub, so the re-exec only added an npx
download on every run — the firewall surface. Users update the CLI via
npm/npx themselves.
- **Publishing** (`scripts/package-dashboard-release.mjs` +
`.github/workflows/dashboard-release.yaml`): build standalone → zip →
write manifest → publish immutable `dashboard-v<version>` release +
clobber the `dashboard-latest` manifest. **Fails loudly** if an existing
tag's asset sha differs from a fresh build (dashboard changed without a
version bump → would otherwise advertise a hash the served zip doesn't
match).
- **Build**: CLI build is now `tsdown`-only (no bundled dashboard);
`turbo.json` drops the dashboard standalone build from the CLI build
graph.
## Testing
- **Unit**: 135 CLI tests pass (manifest parsing incl. path-safe version
validation, URL resolution, version picking, dir override).
- **End-to-end (no GitHub)**: a localhost HTTP server + fixture zip
exercised the full fetch path — download, sha256 verify, cache-hit,
roll-forward, **corrupt-sha rejection**, offline→cache fallback (9/9).
- **Real dashboard, no GitHub**: built the standalone, served the real
76MB zip over `python -m http.server`, and confirmed `hexclave dev`
downloads → verifies → extracts → **boots** the dashboard and registers
an RDE session against the hosted backend. Also validated
`HEXCLAVE_DASHBOARD_DIR` boot.
## Review
Three review agents (runtime correctness, packaging/workflow, code
quality) ran; valid findings fixed: path-traversal hardening on
`version`, a concurrent cache rm/rename race, the workflow
sha-mismatch/atomicity gap, plus `errorMessage` dedup and added tests.
## Notes / follow-ups
- No CLI↔dashboard compatibility gate yet (assumes always-compatible);
easy to add later via a min-CLI field in the manifest. This matters more
now that the CLI no longer auto-updates.
- The publish workflow has not run yet, so the first push to `main` will
create the `dashboard-latest` release.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added automated, immutable versioned releases for the standalone
dashboard plus a continuously updated “latest” manifest.
* CLI now fetches, verifies, caches, and supports offline fallback for
the dashboard runtime.
* **Bug Fixes**
* Improved dashboard restart behavior to update only when a strictly
newer published version is available.
* **Chores**
* Removed the CLI’s automatic update/re-launch behavior for more
predictable startup.
* **Tests**
* Added unit tests for dashboard manifest validation and latest-version
selection.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
80 lines
3.6 KiB
JavaScript
80 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Packages the standalone RDE dashboard into a GitHub Release artifact: a
|
|
// dashboard-<version>.zip plus a manifest.json ({ version, sha256, url }) that
|
|
// dashboard-release.ts fetches at runtime. Run by dashboard-release.yaml;
|
|
// requires the `zip` CLI (present on ubuntu runners).
|
|
import { execFileSync } from "child_process";
|
|
import { createHash } from "crypto";
|
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
import { dirname, join, resolve } from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const packageRoot = resolve(__dirname, "..");
|
|
const repoRoot = resolve(packageRoot, "../..");
|
|
|
|
// Overridable so forks / mirrors can host their own releases.
|
|
const repository = process.env.DASHBOARD_RELEASE_REPO ?? "hexclave/hexclave";
|
|
// For local testing: point the manifest's asset URL at a static server
|
|
// (e.g. http://127.0.0.1:8000) instead of GitHub.
|
|
const baseUrlOverride = process.env.DASHBOARD_RELEASE_BASE_URL?.replace(/\/+$/, "");
|
|
|
|
// Must mirror SAFE_VERSION_REGEX in packages/cli/src/lib/dashboard-release.ts:
|
|
// the CLI rejects any manifest whose version fails this pattern, and the version
|
|
// becomes a release tag and zip filename, so fail loudly here before publishing
|
|
// an artifact every CLI would ignore.
|
|
const SAFE_VERSION_REGEX = /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
const dashboardPackageJson = JSON.parse(readFileSync(join(repoRoot, "apps/dashboard/package.json"), "utf-8"));
|
|
const version = dashboardPackageJson.version;
|
|
if (typeof version !== "string" || !SAFE_VERSION_REGEX.test(version)) {
|
|
throw new Error(`apps/dashboard/package.json has an invalid version ${JSON.stringify(version)}; expected a path-safe semver matching ${SAFE_VERSION_REGEX}.`);
|
|
}
|
|
|
|
const dashboardDist = join(packageRoot, "dist", "dashboard");
|
|
const serverEntry = join(dashboardDist, "apps", "dashboard", "server.js");
|
|
const outDir = join(packageRoot, "dashboard-release");
|
|
const zipName = `dashboard-${version}.zip`;
|
|
const zipPath = join(outDir, zipName);
|
|
const manifestPath = join(outDir, "manifest.json");
|
|
const tag = `dashboard-v${version}`;
|
|
const assetUrl = baseUrlOverride != null && baseUrlOverride.length > 0
|
|
? `${baseUrlOverride}/${zipName}`
|
|
: `https://github.com/${repository}/releases/download/${tag}/${zipName}`;
|
|
|
|
// 1. Stage the standalone dashboard runtime into dist/dashboard.
|
|
execFileSync(process.execPath, [join(__dirname, "copy-runtime-assets.mjs")], { stdio: "inherit" });
|
|
if (!existsSync(serverEntry)) {
|
|
throw new Error(`Expected a staged dashboard server at ${serverEntry}. Did build:rde-standalone run?`);
|
|
}
|
|
|
|
// 2. Zip the staged runtime so the archive root holds apps/ and node_modules/.
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
mkdirSync(outDir, { recursive: true });
|
|
execFileSync("zip", ["-q", "-r", "-X", zipPath, "."], { cwd: dashboardDist, stdio: "inherit" });
|
|
|
|
// 3. Hash the archive and write the manifest the CLI fetches at runtime.
|
|
const sha256 = createHash("sha256").update(readFileSync(zipPath)).digest("hex");
|
|
writeFileSync(manifestPath, `${JSON.stringify({ version, sha256, url: assetUrl }, null, 2)}\n`);
|
|
|
|
console.log(`Packaged dashboard ${version}`);
|
|
console.log(` zip: ${zipPath}`);
|
|
console.log(` sha256: ${sha256}`);
|
|
console.log(` url: ${assetUrl}`);
|
|
console.log(` manifest: ${manifestPath}`);
|
|
|
|
// Expose values to the release workflow.
|
|
if (process.env.GITHUB_OUTPUT) {
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
[
|
|
`version=${version}`,
|
|
`tag=${tag}`,
|
|
`zip=${zipPath}`,
|
|
`zip_name=${zipName}`,
|
|
`sha256=${sha256}`,
|
|
`manifest=${manifestPath}`,
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
}
|