Files
stack/.github/workflows/dashboard-release.yaml
T
BilalG1andGitHub 526ce69ab9 feat(cli): fetch RDE dashboard from GitHub Releases instead of bundling (#1682)
## 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 -->
2026-06-30 16:12:13 -07:00

145 lines
6.7 KiB
YAML

name: Publish RDE dashboard release
# Builds the standalone RDE dashboard and publishes it as a GitHub Release
# artifact (instead of bundling it into the @hexclave/cli npm tarball). The CLI
# fetches the newest build at runtime via the `dashboard-latest` manifest. See
# packages/cli/src/lib/dashboard-release.ts.
on:
push:
branches:
- main
# Only rebuild/publish when the dashboard or its packaging actually changes.
# A new release is keyed by the version in apps/dashboard/package.json, so
# changes outside these paths can't produce a new dashboard build anyway.
paths:
- 'apps/dashboard/**'
- 'packages/cli/scripts/package-dashboard-release.mjs'
- 'packages/cli/scripts/copy-runtime-assets.mjs'
- '.github/workflows/dashboard-release.yaml'
permissions:
contents: write # create releases and upload assets
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false # don't interrupt a publish in progress
jobs:
publish-dashboard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
# Don't leave the contents:write token in .git/config for the
# install/build/package steps; the gh steps below pass GH_TOKEN directly.
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22.x'
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build dashboard standalone
run: pnpm exec turbo run build:rde-standalone --filter=@hexclave/dashboard
- name: Package dashboard release
id: package
env:
DASHBOARD_RELEASE_REPO: ${{ github.repository }}
run: pnpm --filter @hexclave/cli run package-dashboard-release
- name: Publish versioned release (immutable)
id: publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.package.outputs.tag }}
ZIP: ${{ steps.package.outputs.zip }}
ZIP_NAME: ${{ steps.package.outputs.zip_name }}
VERSION: ${{ steps.package.outputs.version }}
EXPECTED_SHA: ${{ steps.package.outputs.sha256 }}
run: |
set -euo pipefail
# A published dashboard-v<version> asset is immutable, so never replace
# it; advertise the sha of whatever asset is live. (The rebuild isn't
# byte-reproducible — staged files get fresh mtimes — so we can't just
# trust EXPECTED_SHA when reusing an existing asset.)
live_sha="$EXPECTED_SHA"
if gh release view "$TAG" >/dev/null 2>&1; then
if gh release view "$TAG" --json assets -q '.assets[].name' | grep -qx "$ZIP_NAME"; then
tmp="$(mktemp -d)"
gh release download "$TAG" -p "$ZIP_NAME" -D "$tmp"
live_sha="$(sha256sum "$tmp/$ZIP_NAME" | cut -d' ' -f1)"
if [ "$live_sha" != "$EXPECTED_SHA" ]; then
echo "::warning::$TAG already has a published asset whose sha256 ($live_sha) differs from this rebuild ($EXPECTED_SHA). Releases are immutable, so the existing asset is kept and advertised. A differing hash is expected for nondeterministic rebuilds of identical content; if you intended to ship NEW dashboard content, bump the version in apps/dashboard/package.json."
else
echo "Existing $TAG asset matches the build; reusing it."
fi
else
echo "$TAG exists without its asset; uploading."
gh release upload "$TAG" "$ZIP" --clobber
fi
else
gh release create "$TAG" "$ZIP" \
--title "RDE dashboard $VERSION" \
--notes "Standalone RDE dashboard build for \`hexclave dev\` (version $VERSION)."
fi
echo "live_sha=$live_sha" >> "$GITHUB_OUTPUT"
- name: Update floating latest manifest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MANIFEST: ${{ steps.package.outputs.manifest }}
VERSION: ${{ steps.package.outputs.version }}
LIVE_SHA: ${{ steps.publish.outputs.live_sha }}
run: |
set -euo pipefail
# Advertise the live asset's sha, which may differ from the rebuild's.
tmp="$(mktemp)"
jq --arg s "$LIVE_SHA" '.sha256 = $s' "$MANIFEST" > "$tmp"
mv "$tmp" "$MANIFEST"
# dashboard-latest is only ever a plain X.Y.Z release. Refuse to advance
# it to a prerelease/build-metadata version rather than guess SemVer
# prerelease ordering in bash (`sort -V` is not a SemVer comparator).
# The dashboard always ships plain releases, so this should never fire.
new="${VERSION#v}"
case "$new" in
*[-+]*)
echo "::error::Refusing to publish prerelease/build version $VERSION as dashboard-latest; it must be a plain X.Y.Z release."
exit 1
;;
esac
if gh release view dashboard-latest >/dev/null 2>&1; then
# dashboard-latest must only ever move forward. Re-running on an older
# commit must not roll clients back to a stale dashboard. Re-publishing
# the same version is allowed (it refreshes the advertised sha).
published_dir="$(mktemp -d)"
if gh release download dashboard-latest -p manifest.json -D "$published_dir" 2>/dev/null; then
published_version="$(jq -r '.version // ""' "$published_dir/manifest.json")"
# Both sides are plain releases (the published one was gated the same
# way), so a leading-'v'-stripped, numeric-aware `sort -V` is exact.
pub="${published_version#v}"
if [ -n "$pub" ] && [ "$pub" != "$new" ]; then
newest="$(printf '%s\n%s\n' "$pub" "$new" | sort -V | tail -n1)"
if [ "$newest" = "$pub" ]; then
echo "::warning::dashboard-latest already points at $published_version, which is newer than $VERSION; refusing to roll it back."
exit 0
fi
fi
fi
else
gh release create dashboard-latest \
--title "RDE dashboard (latest)" \
--notes "Always points at the newest published RDE dashboard. The manifest.json asset is consumed by the Hexclave CLI at runtime." \
--latest=false
fi
gh release upload dashboard-latest "$MANIFEST" --clobber
echo "dashboard-latest now points at $VERSION (sha256 $LIVE_SHA)."