diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 047645c..f3076d9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.25.0" + ".": "1.26.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 309a3ca..27527cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.26.0](https://github.com/dograh-hq/dograh/compare/dograh-v1.25.0...dograh-v1.26.0) (2026-04-21) + + +### Features + +* refactor node spec and add mcp tools ([#244](https://github.com/dograh-hq/dograh/issues/244)) ([00a1a22](https://github.com/dograh-hq/dograh/commit/00a1a22b749dab5b828ee529d7272cbdaaeb9aca)) + + +### Bug Fixes + +* compare dirty against correct baseline ([6606a7f](https://github.com/dograh-hq/dograh/commit/6606a7f901f351d5832ebc27a0900c1195a4090c)) +* fix slack community URL ([86026f5](https://github.com/dograh-hq/dograh/commit/86026f5c6ffdbfbef96cec8aff6c4265f061b77e)) + ## [1.25.0](https://github.com/dograh-hq/dograh/compare/dograh-v1.24.0...dograh-v1.25.0) (2026-04-17) diff --git a/api/pyproject.toml b/api/pyproject.toml index a8c71eb..bb1bae8 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,5 +1,5 @@ [project] name = "dograh-api" -version = "1.25.0" +version = "1.26.0" description = "Backend API for Dograh voice AI platform" requires-python = ">=3.12" diff --git a/docs/deployment/update.mdx b/docs/deployment/update.mdx new file mode 100644 index 0000000..d4dc3a1 --- /dev/null +++ b/docs/deployment/update.mdx @@ -0,0 +1,113 @@ +--- +title: "Update" +description: "Update your self-hosted Dograh stack to a newer image version" +--- + +This guide covers updating a Dograh stack you've already deployed with [Docker](/deployment/docker) or a [custom domain](/deployment/custom-domain). You run commands from the same directory that contains your `docker-compose.yaml` (this is the `dograh/` directory if you used `setup_remote.sh`). + +## Find an image version + +Dograh publishes two images — `dograh-api` and `dograh-ui` — to both container registries: + +- **GitHub Container Registry** — [github.com/orgs/dograh-hq/packages](https://github.com/orgs/dograh-hq/packages) +- **Docker Hub** — [hub.docker.com/u/dograhai](https://hub.docker.com/u/dograhai) + +Each release is published under two kinds of tags: + +| Tag style | Example | When to use | +|-----------|---------|-------------| +| **Release tag** | `v0.8.2` | Stable, recommended for production | +| **Git commit SHA** | `a1b2c3d` | Bleeding edge — any commit merged to `main` | +| `latest` | `latest` | Tracks the most recent release tag | + + +Always update **`dograh-api`** and **`dograh-ui`** to the **same tag**. The two images are built from the same commit and the UI expects API responses in a matching shape — mixing versions will break the app. + + +## Option A: Update to the latest release + +If your `docker-compose.yaml` uses `:latest` (the default), just pull and restart: + + +```bash Local deployment +docker compose down +docker compose up --pull always +``` +```bash Remote deployment +cd dograh +sudo docker compose --profile remote down +sudo docker compose --profile remote up --pull always +``` + + +`--pull always` forces Docker to fetch the latest `:latest` from the registry instead of reusing your cached image. + +## Option B: Pin a specific tag + +To update (or roll back) to a specific release or commit, edit `docker-compose.yaml` and change the `image:` lines for both `api` and `ui` services to the same tag. + +Open the file: + +```bash +nano docker-compose.yaml +``` + +Find these two lines: + +```yaml + api: + image: ${REGISTRY:-dograhai}/dograh-api:latest + ui: + image: ${REGISTRY:-dograhai}/dograh-ui:latest +``` + +Replace `:latest` with your chosen tag on **both** services — for example: + +```yaml + api: + image: ${REGISTRY:-dograhai}/dograh-api:v0.8.2 + ui: + image: ${REGISTRY:-dograhai}/dograh-ui:v0.8.2 +``` + + +You can use either registry. Leave `REGISTRY` unset for Docker Hub (`dograhai`), or export `REGISTRY=ghcr.io/dograh-hq` to pull from GitHub Container Registry. + + +Then bring the stack down and back up: + + +```bash Local deployment +docker compose down +docker compose up --pull always +``` +```bash Remote deployment +cd dograh +sudo docker compose --profile remote down +sudo docker compose --profile remote up --pull always +``` + + +## Verify the update + +Check the running image tags: + +```bash +docker compose ps --format "table {{.Service}}\t{{.Image}}" +``` + +You should see the API and UI both running the tag you pinned. + +Hit the health endpoint to confirm the API is responding: + +```bash +curl http://localhost:8000/api/v1/health +``` + +## Roll back + +If something breaks, roll back by pinning the previous tag using the same process in **Option B** and restarting. Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved. + + +Rolling back across a database migration is not always safe — if the newer release ran a schema migration, downgrading may leave the DB in a state the older API doesn't understand. If in doubt, [open an issue](https://github.com/dograh-hq/dograh/issues) before rolling back. + diff --git a/docs/docs.json b/docs/docs.json index 95fa29a..ddb4405 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -141,6 +141,7 @@ "deployment/introduction", "deployment/docker", "deployment/custom-domain", + "deployment/update", "deployment/web-widget", "deployment/heroku" ] diff --git a/scripts/rolling_update.sh b/scripts/rolling_update.sh index d39e603..244933f 100755 --- a/scripts/rolling_update.sh +++ b/scripts/rolling_update.sh @@ -217,6 +217,15 @@ if ! alembic -c "$BASE_DIR/api/alembic.ini" upgrade head; then fi log_info "Migrations complete" +TS_VALIDATOR_DIR="$BASE_DIR/api/mcp_server/ts_validator" +if [[ -f "$TS_VALIDATOR_DIR/package.json" ]]; then + log_info "Installing ts_validator npm dependencies" + if ! (cd "$TS_VALIDATOR_DIR" && npm install); then + log_error "npm install for ts_validator failed. Aborting — nothing has been touched." + exit 1 + fi +fi + ############################################################################### ### PHASE 2: START NEW WORKERS ############################################################################### diff --git a/scripts/start_services.sh b/scripts/start_services.sh index 4ee9148..c706afb 100755 --- a/scripts/start_services.sh +++ b/scripts/start_services.sh @@ -127,7 +127,16 @@ NGINX_UPSTREAM_TEMPLATE="$BASE_DIR/nginx/dograh_upstream.conf.template" NGINX_UPSTREAM_CONF="/etc/nginx/conf.d/dograh_upstream.conf" ############################################################################### -### 4) Run migrations +### 4) Install ts_validator npm dependencies +############################################################################### + +TS_VALIDATOR_DIR="$BASE_DIR/api/mcp_server/ts_validator" +if [[ -f "$TS_VALIDATOR_DIR/package.json" ]]; then + (cd "$TS_VALIDATOR_DIR" && npm install) +fi + +############################################################################### +### 5) Run migrations ############################################################################### alembic -c "$BASE_DIR/api/alembic.ini" upgrade head diff --git a/ui/package.json b/ui/package.json index f2bbccb..002e7d7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "1.25.0", + "version": "1.26.0", "private": true, "scripts": { "dev": "cross-env NODE_OPTIONS=--enable-source-maps next dev --turbopack", diff --git a/ui/src/components/flow/nodes/GenericNode.tsx b/ui/src/components/flow/nodes/GenericNode.tsx index b8b395e..fcd7fee 100644 --- a/ui/src/components/flow/nodes/GenericNode.tsx +++ b/ui/src/components/flow/nodes/GenericNode.tsx @@ -369,9 +369,10 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps) ); const isDirty = useMemo(() => { - const d = data as unknown as Record; - return propertyNames.some((n) => values[n] !== d[n]); - }, [values, data, propertyNames]); + if (!spec) return false; + const baseline = seedValues(data, spec); + return propertyNames.some((n) => values[n] !== baseline[n]); + }, [values, data, spec, propertyNames]); const handleSave = async () => { if (!spec) return; diff --git a/ui/src/components/layout/AppSidebar.tsx b/ui/src/components/layout/AppSidebar.tsx index 62685e0..8e5870c 100644 --- a/ui/src/components/layout/AppSidebar.tsx +++ b/ui/src/components/layout/AppSidebar.tsx @@ -2,6 +2,7 @@ import type { Team } from "@stackframe/stack"; import { + ArrowUpCircle, AudioLines, Brain, ChevronLeft, @@ -54,6 +55,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useAppConfig } from "@/context/AppConfigContext"; +import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion"; import type { LocalUser } from "@/lib/auth"; import { useAuth } from "@/lib/auth"; import { cn } from "@/lib/utils"; @@ -89,6 +91,12 @@ export function AppSidebar() { // Version info from app config context const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null; + // Check for updates only on self-hosted (OSS) deployments — cloud is managed for the user. + const { latest: latestRelease, isBehind, isLatest } = useLatestReleaseVersion( + versionInfo?.ui, + { enabled: config?.deploymentMode === "oss" }, + ); + const isActive = (path: string) => { return pathname.startsWith(path); }; @@ -232,17 +240,53 @@ export function AppSidebar() {
{/* Logo - only show when expanded */} {effectiveState === "expanded" && ( - - Dograh - {versionInfo && ( - - v{versionInfo.ui} - +
+ + Dograh + {versionInfo && ( + + v{versionInfo.ui} + + )} + + {isBehind && latestRelease && ( + + + + + + Update + + + +

Latest: {latestRelease} — click to see the update guide

+
+
+
)} - + {isLatest && ( + + + + + Latest + + + +

You're running the latest release

+
+
+
+ )} +
)} {/* Toggle button - center it when collapsed */} l[i]) return false; + } + return false; +} + +export function useLatestReleaseVersion( + currentVersion: string | undefined, + { enabled }: Options, +): Result { + const [latest, setLatest] = useState(null); + + useEffect(() => { + if (!enabled || !currentVersion) return; + + try { + const raw = localStorage.getItem(CACHE_KEY); + if (raw) { + const parsed = JSON.parse(raw) as { tag: string; fetchedAt: number }; + if (Date.now() - parsed.fetchedAt < CACHE_TTL_MS) { + setLatest(parsed.tag); + return; + } + } + } catch { + // ignore malformed cache + } + + let cancelled = false; + fetch("https://api.github.com/repos/dograh-hq/dograh/releases/latest") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { + if (cancelled || !data?.tag_name) return; + const tag = data.tag_name as string; + try { + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ tag, fetchedAt: Date.now() }), + ); + } catch { + // storage may be full or disabled + } + setLatest(tag); + }) + .catch(() => { + // silent — don't break the sidebar if GitHub is unreachable + }); + + return () => { + cancelled = true; + }; + }, [enabled, currentVersion]); + + const normalizedCurrent = currentVersion + ? currentVersion.startsWith("v") + ? currentVersion + : `v${currentVersion}` + : null; + + const currentParsed = normalizedCurrent ? parseSemver(normalizedCurrent) : null; + const latestParsed = latest ? parseSemver(latest) : null; + + const isBehind = !!( + normalizedCurrent && + latest && + isOlder(normalizedCurrent, latest) + ); + + const isLatest = !!( + currentParsed && + latestParsed && + currentParsed[0] === latestParsed[0] && + currentParsed[1] === latestParsed[1] && + currentParsed[2] === latestParsed[2] + ); + + return { latest, isBehind, isLatest }; +}